Android Jetpack Compose: Clean Architecture — 2022 (Prima Version)

Prima
4 min readApr 17, 2022
Photo by Jeremy Thomas on Unsplash

Based on the Guide to app architecture by Google for Android Developers and combined with the principals of Clean Arhitecture. This is how a project for Android in majority looks like if they want/follow Clean Architecture. Some may have it a little differently in the subfolders/subpackages. In general you will see at least the 3 major packages on the top level.

Don’t overkill it with the cleanness. If your app is small, you can skip the domain layer. If you just add code, to follow a “best practice”, don’t. You will just waste time and resources. If your app has like 100 screens, then yes, the “best practice” will be useful. Just ask yourself “will implement x best practice really help me?”.

In any case, I do recommend reading the Guide to app architecture by Google on the Android Developer page. What is about to follow is a super short version and if you are first time reading about architecture, this probably won’t help you much. At the bottom, check the link to the Android Developer Youtube channel on Arhitecture.

The 3 layers

The bread and butter. Every app should have these 3 packages: ui, data, domain.

On the Android guide page they have a good picture to represent the flow between these 3.

UI layer

Everything in here is meant for displaying something on the screen. Includes: View Models, Ui State holders, Ui elements.

Most common layout in the ui package (example showing HomeScreen as one of the screens).

ui
- theme
- navigation
- screens
- HomeScreen
- HomeScreen
- HomeViewModel
- HomeUiState
- components

HomeScreen is your typical compose function that contains the view model.

@Composable
fun HomeScreen(
val viewModel: HomeViewModel by viewModel()
){
val uiState = viewModel.uiState
Text("Hello ${uiState.name}")
Button(onClick = {viewModel.changeName()}){ Text("Change Name") }
}

HomeViewModel is your typical View Model that holds the ui state holder.

class HomeViewModel: ViewModel() {
val uiState: HomeUiState by mutableStateOf(HomeUiState())
fun changeName(){
uiState = uiState.copy(name = "New Name")
}
}

HomeUiState is a state holder, and holds all the data that would be displayed on your UI for the screen Home. All of its data are immutable. You change the values of it by using the copy method (see view model).

data class HomeUiState(
val name:String = "Unknown"
)

Now whenever the name or any property of the HomeUiState changes, its reflected on the UI as well.

Domain layer

The place where you put your use case classes and your repository interfaces (not implementations), and the models that your app will use in this project. Includes: UseCase, Repository, Model

This time using an example to get authors.

domain
- usecase (verb in present tense + noun/what (optional) + UseCase)
- GetAuthorsUseCase
- repository (interfaces only)
- AuthorsRepository
- model
- Author

The model is not your API or DB record representation. Its your business logic model.

data class Author(
val id:Int,
val name:String
)

Repositories in here are just interfaces

interface AuthorsRepository(){
suspend fun getAuthors(): List<Author>
}

And finally the use cases. Ui layer will use these. Now if your app is small in scale, this is just boilerplate code. It’s meant for medium to large applications.

class GetAuthorsUseCase(
private val authorsRepository: AuthorsRepository
) { ... }

Data layer

Here you write your repository implementations of the interfaces that are written in Domain layer. Is reponsible for anything data related. Getting, storing, local db, network calls … all in here.

Includes: Repository, DataSource, WorkManagers, Mappers, …

data
- repository
- AuthorsRepository
- data source
- AuthorsLocalDataSource
- AuthorsRemoteDataSource
- local
- entity
- dao
- database
- remote
- dao
- retrofit
- work managers*- mappers (from db/remote model to domain model we want to use in our app/domain)

The actual repository implementation.

class AuthorsRepositoryImp(...) : AuthorsRepository {
override suspend fun getAuthors()...
}

The 4th package

Dependency Injection. This package is called just di. Use dagger hilt. Includes: modules

The 5th package

Most common package, the util. Contains any helper functions that any code can use. Constants, formaters, etc…

Domain Features

Domain here is not the domain mentioned above. This is the “domain of operations” or features of your app. For example, an app about selling flowers, has the following “domain of operations”: authentication, flowers, orders. Each of these is “responsible” to do things inside its domain of operations. Authentication does not need logic or ui about flowers, for example.

Recommended project structure would be like this:

app
- feature_auth
- ui
- domain
- data
- feature_flowers
- ui
- domain
- data
- feature_orders
- ui
- domain
- data

Summary

ui
- screens
- ui states
- view models
- ui elements
data
- repository
- data sources
- mappers
- models
domain
- repository
- use cases
- models
di
- modules
util
- constants
- formaters
- ...

Useful

Android Developers YouTube channel — Architecture playlist

--

--