Getting Started With Clean Architecture for Android [Part 2]

David Takač
COBE
Published in
7 min readMar 1, 2023

--

This article was previously published at cobeisfresh.com.

Welcome back and I hope you’re ready for more lines of code. Last time we talked about the basics of Clean architecture and how it can help you separate the domain and presentation logic. Now, it’s time to get more advanced.

Keep reading and find out how the modules are wired together with Gradle, how Dagger Hilt provides the dependencies, and how to implement the presentation layer in the app module.

Gradle files

Before we get into the app module that wires everything together, you need to understand the Gradle files of the other modules first. They’ll make dependency management clearer and demonstrate depending in the direction of stability.

plugins {
id 'java-library'
id 'kotlin'
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

Let’s start with the domain module’s Gradle file, as shown above. You can see that it doesn’t have an android block, which means that it’s independent of the Android framework. It has no dependencies block either, which means that it doesn’t depend on anything. Of course, this is an ideal case that's only possible because the app is quite simple, but it's something you should strive towards nonetheless. When you feel tempted to add dependencies to it, think about creating an interface for that behavior and implementing it in a separate module instead – as we did for persistence and recognizer.

plugins {
id 'java-library'
id 'kotlin'
}

java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}

dependencies {
implementation project(':domain')

// Network
implementation "com.google.code.gson:gson:$versions.gson"
implementation "com.squareup.retrofit2:retrofit:$versions.retrofit"
implementation "com.squareup.retrofit2:converter-gson:$versions.retrofit"
implementation "com.squareup.okhttp3:logging-interceptor:$versions.okhttp"
}

The auddrecognizer module’s Gradle file is a bit more complex, but don’t let it scare you off. It’s also a pure Kotlin module as it doesn’t need any Android APIs to implement network communication. It depends on the domain module because it implements its SongRecognizer interface using Retrofit, OkHttp, and Gson to communicate with Audd's servers.

plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
}

android {...}

dependencies {
implementation project(':domain')

// Room
implementation "androidx.room:room-ktx:$versions.room"
implementation "androidx.room:room-runtime:$versions.room"
kapt "androidx.room:room-compiler:$versions.room"

// Core library desugaring
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$versions.core_library_desugaring"
}

The roompersistence module’s Gradle file is the most complex so far, but only because Room depends on the Android framework. Since the code in blocks plugins and android is mainly boilerplate, you can ignore it here. The thing we're really interested in is the dependencies block, which is similar to the previous modules. You can see it also depends on the domain module because it implements the SongSaver and SongGetter interfaces using Room to communicate with Android's built-in SQLite database. It also implements core library desugaring, so it can use Java 8's excellent time API.

To implement the abstract behaviors of the domain module, roompersistence and auddrecognizer need to depend on it. This is a clear example of depending in the direction of stability — the volatile code depends on and molds itself around the contracts set by the stable code. This way, when the volatile code changes, the stable code will remain unaffected. It also doesn’t need to be recompiled, and this will save you build time.

The app module

Every program needs an entry point that starts its execution. One of the simplest ways to achieve this is in command line apps, which often have a main function that initializes the program’s dependencies and then runs, taking and displaying input and output text. Android apps will go a few steps further and provide a graphical user interface that does similar things but in a more user-friendly way. They implement conventional main function behaviors with Android-specific mechanisms, like Activities, Fragments, and Services. These user-facing classes initialize dependencies, gather input, and display output with Views in many different and attractive ways.

In other words, Android code is like a complex main function. It should provide every dependency, react to user input and render the output. It’s also highly prone to change due to external events like Google deciding to deprecate an API, or a library deciding to remove a feature that you depend on.

Remember, none of these things are in our control. They all happen because a third party decided so, and your only option here is to adapt. That is why the main code, and by extension Android code, is the most volatile code in Clean Architecture. By comparison, the changes in the domain code occur only if our project team wants them to. Therefore, you should completely separate your domain code from Android. Keeping them independent will allow you to modify them separately. To do this, you need to put all of the code that concerns Android and dependency management into the app module.

└── app
├── HortonApp.kt
├── di
│ ├── AppModule.kt
│ └── ViewModelModule.kt
├── presentation
│ ├── coroutines
│ │ ├── AndroidDispatcherProvider.kt
│ │ └── DispatcherProvider.kt
│ ├── history
│ │ └── HistoryViewModel.kt
│ ├── mappers
│ │ └── RecognizedSongMapper.kt
│ └── recognition
│ └── RecognitionViewModel.kt
└── ui
├── composables [...]
├── history
│ ├── HistoryActivity.kt
│ ├── composables [...]
│ └── model [...]
├── recognition
│ ├── RecognitionActivity.kt
│ ├── composables [...]
│ ├── model [...]
│ └── recorder [...]
└── theme [...]

You can see its directory structure above, and here are three top-level packages:

  • di in which dependency injection is performed with Dagger Hilt,
  • presentation in which ViewModels map domain data to UI data, and
  • ui which renders the user interface based on this data and records audio. We won’t go over this package here because it’s just ordinary Jetpack Compose and MediaRecorder code.
plugins { ... }
android { ... }

dependencies {
implementation project(":roompersistence")
implementation project(":auddrecognizer")
implementation project(":domain")

// Room
implementation "androidx.room:room-ktx:$versions.room"

// Network
implementation "com.google.code.gson:gson:$versions.gson"
implementation "com.squareup.retrofit2:retrofit:$versions.retrofit"
implementation "com.squareup.retrofit2:converter-gson:$versions.retrofit"
implementation "com.squareup.okhttp3:logging-interceptor:$versions.okhttp"

// Dependency injection
implementation "com.google.dagger:hilt-android:$versions.dagger"
kapt "com.google.dagger:hilt-compiler:$versions.dagger"

// Android coroutines
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$versions.coroutines"

// Lifecycle and extensions
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$versions.lifecycle"
implementation "androidx.activity:activity-ktx:$versions.activity_ktx"

// Compose
implementation "androidx.compose.ui:ui:$versions.compose"
// (...) other Compose dependencies like Material, Coil etc.

// Core library desugaring
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:$versions.core_library_desugaring"
}

This is its Gradle file. The plugins and android blocks were collapsed for brevity as they only contain the usual boilerplate every app module has. What's interesting here is the dependencies block. Because the app module is our equivalent of the main function, it needs to see every module in the project so that it can wire together their dependencies and make them run. It also implements the stuff it needs to render the UI, like Android Coroutines, Jetpack Compose, and Lifecycle extensions.

Dependency injection

Theoretically, dependency injection is a low-level implementation detail that is only a more sophisticated way of constructing and providing the dependencies your code needs. Therefore, it should be easy to replace and you shouldn’t pollute your codebase with it. A class shouldn’t know that its dependencies are injected with any particular library nor that it will be a singleton, for example. This is not its responsibility — it should only know that it can get its dependencies from its constructor. But putting this theory to use can result in a considerable amount of boilerplate code.

To make all the other modules agnostic of the way their dependencies are injected, we need to exclude @Inject. This results in writing code as the following, even though we made both the AuddSongRecognizer and RecognizeSongUseCase classes.

@Provides
fun provideSongRecognizer(
apiInterface: AuddApiInterface
): SongRecognizer {
return AuddSongRecognizer(apiInterface)
}

@Provides
@ViewModelScoped
fun provideRecognizeSongUseCase(
songRecognizer: SongRecognizer,
songSaver: SongSaver
): RecognizeSongUseCase {
return RecognizeSongUseCase(songRecognizer, songSaver)
}

Although it’s not the worst thing that can happen on some smaller projects, you should ask yourself this on bigger ones: are you willing to sacrifice the ease of use @Inject brings for theoretical purity? If your answer is no, you can minimize the pollution by depending only on javax.inject and confining Dagger to the app module only. This will allow Dagger to see and provide the annotated classes, but the module won't have to know that it's using Dagger. It will only know that it's using some dependency injection library compatible with javax.inject.

ViewModels

The last piece of the puzzle is connecting the created use cases to ViewModels that will map their data to UI models, which can be displayed with your library of choice. The thing that interests us here is bridging the gap between the domain and UI layers.

@HiltViewModel
class RecognitionViewModel @Inject constructor(
private val recognizeUseCase: RecognizeSongUseCase,
private val recorder: Recorder
) : ViewModel() {
private val mutableState = mutableStateOf(RecognitionScreenState())
val state: State<RecognitionScreenState> get() = mutableState

fun startRecording() {
viewModelScope.launch {
mutableState.value = mutableState.value.copy(buttonEnabled = false)
recorder.start()
recordingCountdown()
recognize(filePath = recorder.stop())
}
}

private fun recognize(filePath: String) {
viewModelScope.launch {
mutableState.value = mutableState.value.copy(
progressBarState = ProgressBarState.Indeterminate
)

val result = recognizeUseCase.recognize(filePath)
if (result is RecognizeSongResult.Success) {
mutableState.value = mutableState.value.copy(
statusState = StatusState.HIDDEN,
recognitionState = RecognitionState.Recognized(result.recognizedSong.mapToSongUiModel())
)
} else {
mutableState.value = mutableState.value.copy(
statusState = StatusState.NOT_RECOGNIZED,
)
}

mutableState.value = RecognitionScreenState(
recognitionState = mutableState.value.recognitionState,
statusState = mutableState.value.statusState
)
}
}

// omitted recording helpers
}

Here we can see the RecognitionViewModel, which controls the screen responsible for recording audio and recognizing it. It has a public method that starts the recording process and a public value that represents the screen state. The method is invoked on button press, and it starts the following flow:

  1. Record audio for recordingCountdown amount of time, determined from sampleDuration
  2. Invoke the use case by passing it the newly-recorded audio file
  3. Map its result to the screen state
  4. Post this state to a mutable state object which the UI can react to

The ViewModel only serves to kick off use cases, map their response to UI data and expose it to the view in a lifecycle-aware way. This is what most ViewModels should look like in Clean architecture — they’re the glue between your UI and domain logic.

In this case, the ViewModel embodies both the controller and presenter components of Clean Architecture.

Conclusion

And that’s it! I hope you enjoyed part 1 and part 2 of the article, and learned something new. Even though you can use the structure from this post, I strongly recommend reading the book as well. Before I read it, I molded my personal projects around the libraries that I would use. Now, I first think of their purpose and requirements and then pick the libraries to match.

This is a very important paradigm shift that will make your code more stable, testable and flexible. If you could use more of that in your projects, wait no longer, and read the book!

--

--