Developing Android Apps with Kotlin and Clean Architecture
2019-09-06 | 11 min read

Developing Android Apps with Kotlin and Clean Architecture

Luka Kordić

Android Developer

The purpose of this article is to showcase an example of Android application architecture with focus on the Clean Architecture approach, originally proposed by Uncle Bob

Why Clean? 

Uncle Bob’s famous onion shaped representation of Clean Architecture

There are a few well-known architectural patterns used in the Android community. I’m sure most developers have sooner or later tried using MVC, MVP or MVVM and found that one of those patterns suits their needs best —  and we are no exception. MVP was our go to solution for a long time, for separating UI code from the rest of the app. 

After Google released Architecture Components we’ve embraced their new guide to app architecture using the MVVM pattern and newly released components. We are still using it for our presentation layer but what we lacked was consistent architecture for other parts of our apps. This is where Clean Architecture comes in.

What is it? 

The main purpose of this approach is a separation of concerns. In order to achieve this, we need to follow one simple but strict rule — the Dependency Rule, represented by those 3 arrows pointing to the center of the graph. It states that:

…source code dependencies can only point inwards. Nothing in an inner circle can know anything at all about something in an outer circle. That includes, functions, classes, variables, or any other named software entity.

I won’t really delve deep into the theory behind Clean Architecture in this post and will instead focus more on its concrete implementation inside an Android application. The most important thing we need to remember is the rule above. It’s the cornerstone of this approach and by following it, we ensure that all layers inside our app are decoupled. Meaning that the business logic will never get in touch with something from Android world, database or network. Another great thing is that it also forces us to use Dependency inversion principle — the “D” in SOLID principles. 

Check out this article if you want to read some in-depth theory behind this approach.

From an Android perspective

As you can see, there are 4 circles on the original graph but there is no rule that says how many layers should exist. This solely depends on the complexity of your projects or you team’s needs. We found that dividing projects into 3 separate modules works for us most of the time. Those 3 modules are:

  • app — Android module
  • domain — this can be a pure Kotlin module because it has no dependencies on the system
  • data — another Kotlin module

Our app module contains all of the code related to the Presentation layer and all the things from Android SDK. This includes things like Activities, Fragments, ViewModels, Services, etc. We are using the MVVM architectural pattern to separate the UI code from the rest of the app, coupled with Android Architecture Components. What we really like about AAC is that they really make observing and handling Android lifecycle so much easier. Also, ViewModel class is ideal for storing a view’s state because it survives configuration changes.

Note: You can use just one module and structure your projects using packages, but it’s easier not to violate the clean architecture dependency rule by using modules.

Domain module holds all of the application’s business logic. This module is the core part of every app and it should be clear what the project is all about just by looking at these files/classes. Following the dependency rule, the domain shouldn’t depend on anything from the app or data module and should also be independent of any framework. That’s why it is the innermost layer in the onion diagram. 

We hold our UseCases and business models in this module. Additionally, we also store repository abstractions(interfaces) here to be able to communicate with our data layer by following the Dependency Inversion Principle. This gives us the possibility to communicate with concrete repository implementations in data module without breaking the dependency rule. This means that we’re basically abstracting away all the logic of the app into these UseCases, which are then implemented in data module, and eventually used in app module.

In data module we hold all concrete implementations of our repositories and other data sources like databases or network, split into corresponding packages. We use repositories as data providers for other layers of our application. The general idea is that other layers don’t know where the data is coming from. All they know is that there is a Repository abstraction that knows how to obtain needed data. And whenever we need some data, either from backend, database or file system we ask repository to get it for us. Great thing about this is that all you have to do is call a method from repository abstraction in the domain layer without knowing any of the implementation details. Fetching data from the network and communication with the database or file system is all hidden from you.

Let me show you an example

I will write a very basic WeatherApp that fetches some data from the api, stores it into the database and displays few pieces of information on the screen. I’ll use OpenWeather for retrieving weather data and Room persistence library as my database. I will also use Koin as a DI framework to wire it all up. 

I’m so sorry, this was a lot of theory to take in, but now comes the fun part — writing code!

Gradle setup

First things first, we need to connect our modules using gradle. Since the app module is the outermost circle in the onion diagram, it knows about the other two modules in our project. We need to add these two lines to our module-level build.gradle file:

   implementation project(":domain")
implementation project(":data")

In the data module’s build.gradle file we only add dependency to the domain module…

    implementation project(':domain')

…and as I said earlier, the domain module shouldn’t really know about anything else other than itself so we don’t have to make any changes to its build.gradle file regarding module setup.

Domain layer

As the domain module is the core of our app, let’s start here. 

domain module structure inside Android Studio

We have the di package here, which holds the providers for our UseCases (or Interactors, if that’s how you want to call them). 

I’m using Koin for the dependency graph setup here. If you haven’t tried it yet I highly encourage you to give it a try. It’s much more simpler to use compared to Dagger 2 and very easy to start with. Assuming you are using Kotlin, of course.

The Interaction package holds our business logic classes. One can know exactly what is happening inside our project simply by looking at the class names of these UseCases. Every UseCase has its interface which serves as the use case output port. This enables us to call them from ViewModels that reside in presentation layer, i.e. app module.

The Model package contains all of the business logic models. Generally we like to separate our data models by their respective scopes. That means that we have separate models for working with the database, another group of models for network communication and group of models used in business logic and UI. By doing this we can filter out invalid data and ensure that we only have what’s really needed at the given time. 

The Repository package has repository abstractions stored in it. These abstractions serve as use case input ports. Concrete implementations of these interfaces live in the data module. 

This is how our use case for retrieving weather data looks like:

class GetWeatherUseCaseImpl(private val weatherRepository: WeatherRepository) : GetWeatherUseCase { 
override suspend operator fun invoke(location: String) = weatherRepository.getWeatherForLocation(location)
}
GetWeatherUseCaseImpl.kt hosted with ❤ by GitHub

Here we are passing the weatherRepository instance through the constructor and calling its getWeatherForLocation method to get weather data back. We marked the method as suspend as it’s going to be called from inside a coroutine. We’re also using Kotlin’s operator overloading feature in order to get a bit shorter and nicer syntax when invoking this use case from our ViewModel.

Data layer

Let’s go ahead and examine data layer of our simple WeatherApp.

 data module structure inside Android Studio

I won’t go into too much detail here. Rather, we are just going to go through a few of the files in order to examine the flow of data and connection between data and domain layers. 

WeatherApi

interface WeatherApi {
@GET("weather")
suspend fun getWeatherForLocation(@Query("q") location: String, @Query("appid") apiKey: String = API_KEY): Response<WeatherInfoResponse>
}
WeatherApi.kt hosted with ❤ by GitHub

Nothing special about this one. Just a regular Retrofit interface with one method for retrieving weather data from the api. We mark the method with suspend modifier and return data back as Response<WeatherInfoResponse> to be able to retrieve metadata about the response. 

WeatherInfoResponse

data class WeatherInfoResponse(val id: Int? = 0,
val weather: List<Weather>?,
val main: MainInfo?,
val name: String? = "") : RoomMapper<WeatherEntity> {
override fun mapToRoomEntity() = WeatherEntity(id ?: 0, weather ?: emptyList(), main ?: MainInfo(), name)
}
data class MainInfo(val temp: Double? = 0.0,
val pressure: Double? = 0.0,
val humidity: Int? = 0)
data class Weather(val id: Int? = 0,
val main: String? = "",
val description: String? = "",
val icon: String? = "")
WeatherInfoResponse.kt hosted with ❤ by GitHub

This is our network response model. It implements RoomMapper interface to be able to transform its data into WeatherEntity object before storing it into the database.

WeatherEntity

@Entity(tableName = WEATHER_TABLE_NAME)
data class WeatherEntity(@PrimaryKey val id: Int? = 0,
val weather: ArrayList<Weather>?,
@Embedded
val main: MainInfo?,
val name: String? = "") : DomainMapper<WeatherInfo> {
override fun mapToDomainModel() = WeatherInfo(main?.temp ?: 0.0, main?.humidity ?: 0, main?.pressure ?: 0.0)
}
WeatherEntity.kt hosted with ❤ by GitHub

WeatherEntity is a data class which is stored into Room database. It implements DomainMapper so we can map it into WeatherInfo domain model before passing it to the use case.

WeatherRepositoryImpl

class WeatherRepositoryImpl(private val weatherApi: WeatherApi,
private val weatherDao: WeatherDao) : BaseRepository<WeatherInfo, WeatherEntity>(), WeatherRepository {
override suspend fun getWeatherForLocation(location: String): Result<WeatherInfo> {
return fetchData(
apiDataProvider = {
weatherApi.getWeatherForLocation(location).getData(
fetchFromCacheAction = { weatherDao.getWeatherInfoForCity(location) },
cacheAction = { weatherDao.saveWeatherInfo(it) })
},
dbDataProvider = { weatherDao.getWeatherInfoForCity(location) }
)
}
}
WeatherRepositoryImpl.kt hosted with ❤ by GitHub

abstract class BaseRepository<T : Any, R : DomainMapper<T>> : KoinComponent {
private val connectivity: Connectivity by inject()
private val contextProvider: CoroutineContextProvider by inject()
/**
* Use this if you need to cache data after fetching it from the api, or retrieve something from cache
*/
protected suspend fun fetchData(
apiDataProvider: suspend () -> Result<T>,
dbDataProvider: suspend () -> R
): Result<T> {
return if (connectivity.hasNetworkAccess()) {
withContext(contextProvider.io) {
apiDataProvider()
}
} else {
withContext(contextProvider.io) {
val dbResult = dbDataProvider()
if (dbResult != null) Success(dbResult.mapToDomainModel()) else Failure(HttpError(Throwable(DB_ENTRY_ERROR)))
}
}
}
/**
* Use this when communicating only with the api service
*/
protected suspend fun fetchData(dataProvider: () -> Result<T>): Result<T> {
return if (connectivity.hasNetworkAccess()) {
withContext(contextProvider.io) {
dataProvider()
}
} else {
Failure(HttpError(Throwable(GENERAL_NETWORK_ERROR)))
}
}
}
BaseRepository.kt hosted with ❤ by GitHub

/**
* Use this if you need to cache data after fetching it from the api, or retrieve something from cache
*/
inline fun <T : RoomMapper<R>, R : DomainMapper<U>, U : Any> Response<T>.getData(
cacheAction: (R) -> Unit,
fetchFromCacheAction: () -> R): Result<U> {
try {
onSuccess {
val databaseEntity = it.mapToRoomEntity()
cacheAction(databaseEntity)
return Success(databaseEntity.mapToDomainModel())
}
onFailure {
val cachedModel = fetchFromCacheAction()
if (cachedModel != null) Success(cachedModel.mapToDomainModel()) else Failure(HttpError(Throwable(DB_ENTRY_ERROR)))
}
return Failure(HttpError(Throwable(GENERAL_NETWORK_ERROR)))
} catch (e: IOException) {
return Failure(HttpError(Throwable(GENERAL_NETWORK_ERROR)))
}
}
/**
* Use this when communicating only with the api service
*/
fun <T : DomainMapper<R>, R : Any> Response<T>.getData(): Result<R> {
try {
onSuccess { return Success(it.mapToDomainModel()) }
onFailure { return Failure(it) }
return Failure(HttpError(Throwable(GENERAL_NETWORK_ERROR)))
} catch (e: IOException) {
return Failure(HttpError(Throwable(GENERAL_NETWORK_ERROR)))
}
}
getData.kt hosted with ❤ by GitHub

Repository implementation is slightly more complex compared to the rest of the code we have written by now, but bear with me here. :)

Most of the time the code in repositories will be the same. So it would be a good idea to make this reusable somehow. In order to achieve that reusability, we’ve extracted some of the code into the BaseRepository and some of the code into the generic getData() extension function.

Let’s examine the BaseRepository class first. There is only one method defined here:

protected suspend fun fetchData(
apiDataProvider: suspend () -> Result<T>,
dbDataProvider: suspend () -> R
): Result<T>

When calling this method, we pass it two lambdas that represent data providers for api and database. Then, in the method’s body we decide which one to run, based on the network state. Before every request we want to check for network connection. To do that, we inject the object of Connectivity class and call its hasNetworkAccess() method. Furthermore, we want every api request or db query to be executed on a background thread. In order to achieve that, we inject CoroutineContextProvider and pass coroutineContext.io to withContext call.

Okay, with the code above we have extracted some of the repeating behaviour regarding threading and network state checking to the BaseRepository, but there are still some things we can generalise. Let’s check out the following extension function…

inline fun <T : RoomMapper<R>, R : DomainMapper<U>, U : Any> Response<T>.getData(
cacheAction: (R) -> Unit,
fetchFromCacheAction: () -> R): Result<U>

It’s a generic function which takes in three model types: response, database and the networking model. It also takes two lambda arguments: one defines caching action and another one defines how we want to fetch data from cache. In our case, when the request completes successfully, we invoke cacheAction(), which caches the response to the database and returns Success case with already mapped data. When the request has failed, fetchFromCacheAction() is executed. If data exists in cache, it is mapped and returned in a Success case. Otherwise, we return Failure case with an error.

With this, we have completely extracted all of the repeating code from the concrete repository implementation. All we have to do now is call the fetchData from BaseRepository and everything else has already been done for us.

One thing left to mention here is the Result<T> type that is returned from the repository method.

sealed class Result<out T : Any>
data class Success<out T : Any>(val data: T) : Result<T>()
data class Failure(val httpError: HttpError) : Result<Nothing>()
class HttpError(val throwable: Throwable, val errorCode: Int = 0)
inline fun <T : Any> Result<T>.onSuccess(action: (T) -> Unit): Result<T> {
if (this is Success) action(data)
return this
}
inline fun <T : Any> Result<T>.onFailure(action: (HttpError) -> Unit) {
if (this is Failure) action(httpError)
}
Result.kt hosted with ❤ by GitHub

Result is a simple construct that represents Success and Failure case. To construct this, we’ve used the power of Kotlin sealed classes. These are basically extension of enum classes. Unlike enums, which can exist only as a single instance, subclasses of sealed class can have multiple instances which can contain state. This makes them ideal for what we want to accomplish. We have also defined onSuccess and onFailure extension functions on the Result type for nicer syntax which you will see a bit later.

WeatherInfo

When we finally get our data back to the calling function in domain layer this is how it looks like:

data class WeatherInfo(val temperature: Double, val humidity: Int, val pressure: Double)
WeatherInfo.kt hosted with ❤ by GitHub

It looks so much nicer and cleaner than those networking and database models with nullable types and annotations. We can now work with this model in domain and presentation layer of our app without any worries of unexpected NPE’s or invalid data being shown.

Presentation layer

We’ve seen how the domain and data layers do their part of work in obtaining data. Now it’s time to pass that data back to the presentation layer and display it to the user.

WeatherViewModel

First, let’s create BaseViewModel abstract class which will, similar to BaseRepository, contain some repeating code.

abstract class BaseViewModel<T : Any, E> : ViewModel(), KoinComponent {
protected val coroutineContext: CoroutineContextProvider by inject()
private val connectivity: Connectivity by inject()
protected val _viewState = MutableLiveData<ViewState<T>>()
val viewState: LiveData<ViewState<T>>
get() = _viewState
protected val _viewEffects = MutableLiveData<E>()
val viewEffects: LiveData<E>
get() = _viewEffects
protected fun executeUseCase(action: suspend () -> Unit, noInternetAction: () -> Unit) {
_viewState.value = Loading()
if (connectivity.hasNetworkAccess()) {
launch { action() }
} else {
noInternetAction()
}
}
protected fun executeUseCase(action: suspend () -> Unit) {
_viewState.value = Loading()
launch { action() }
}
}
BaseViewModel.kthosted with ❤ by GitHub

Here, we inject CoroutineContextProvider and Connectivity objects and define two live data properties which are going to be observed from our views. We use one to store the view state and the other one for various view effects. Additionally, we have defined theexecuteUseCase() method that is going to be called from concrete ViewModel classes whenever there’s a need to invoke a certain UseCase. By doing this, we make sure that every UseCase is ran inside a coroutine and has network state check already built in.

class WeatherViewModel(private val getWeather: GetWeatherUseCase) : BaseViewModel<WeatherInfo, WeatherViewEffects>() {
fun getWeatherForLocation(location: String = DEFAULT_CITY_NAME) = executeUseCase {
getWeather(location)
.onSuccess { _viewState.value = Success(it) }
.onFailure { _viewState.value = Error(it.throwable) }
}
}
WeatherViewModel.kt hosted with ❤ by GitHub

Inside of our ViewModel class we can now simply call executeUseCase() method and pass it actual UseCase invocation as an argument. Here you can see those two extension functions from the previous chapter in action. They allow us to write a concise, 2-line chain for handling success and error cases.

We have created ViewState sealed class that holds our entire screen state.

sealed class ViewState<out T : Any>
class Success<out T : Any>(val data: T) : ViewState<T>()
class Error<out T : Any>(val error: Throwable) : ViewState<T>()
class Loading<out T : Any> : ViewState<T>()
class NoInternetState<T : Any> : ViewState<T>()
ViewState.kt hosted with ❤ by GitHub

It is a really simple class which has 4 states that we can switch by in our Activity or Fragment and update UI accordingly.

When the data arrives we provide it to the view via weatherLiveData object that holds ViewState<WeatherInfo> .

WeatherFragment

class WeatherFragment : BaseFragment() {
private val viewModel: WeatherViewModel by viewModel()
override fun getLayout() = R.layout.fragment_weather
override fun viewReady() {
viewModel.getWeatherForLocation()
subscribeToData()
getWeather.onClick {
weatherFragmentContainer.hideKeyboard()
viewModel.getWeatherForLocation(cityInput.text.toString())
}
showWeatherDetails.onClick { appFragmentNavigator.showWeatherDetails() }
}
private fun subscribeToData() {
viewModel.viewState.subscribe(this, ::handleViewState)
}
private fun handleViewState(viewState: ViewState<WeatherInfo>) {
when (viewState) {
is Loading -> showLoading(weatherLoadingProgress)
is Success -> showWeatherData(viewState.data)
is Error -> handleError(viewState.error.localizedMessage)
is NoInternetState -> showNoInternetError()
}
}
private fun handleError(error: String) {
hideLoading(weatherLoadingProgress)
showError(error, weatherFragmentContainer)
}
private fun showNoInternetError() {
hideLoading(weatherLoadingProgress)
snackbar(getString(R.string.no_internet_error_message), weatherFragmentContainer)
}
private fun showWeatherData(weatherInfo: WeatherInfo) {
hideLoading(weatherLoadingProgress)
temperature.text = convertKelvinToCelsius(weatherInfo.temperature)
pressure.text = weatherInfo.pressure.toString()
humidity.text = weatherInfo.humidity.toString()
}
companion object {
fun newInstance() = WeatherFragment()
}
}
WeatherFragment.kt hosted with ❤ by GitHub

First line in fragment is injecting our WeatherViewModel. Yeah, it’s that simple with Koin. Then in the viewReady method we call viewModel to retrieve the weather data and start observing weatherLiveData object. When data arrives it will trigger the observer and update UI with the latest weather info, or show an error if something went wrong.

Conclusion

Clean architecture is not that hard to implement in an Android project but it still takes some time to wrap your head around it. Once you get used to it though, you will find that it’s easier to maintain your codebase and also add new features in the project. Just by knowing which part of your code goes where, can save you a lot of time and thinking. Your code will also be easier to test since everything is separated. Testing is not covered in this article, but a new one might pop up soon. So, stay tuned. :)

You can check the entire project code here.

Thank you very much for reading. Cheers! Also, a big thanks to Filip Babic!

Like what you read?Go on, share it with friends!
ABOUT THE AUTHOR

Luka Kordić

Android Developer
Luka Kordić is an Android developer at COBE Osijek. He mostly uses Kotlin in his day to day development, but Java is also an option. When he's not writing Android apps, he likes to learn new things from the computer science world. At the moment, he is trying to learn Python and machine learning. He really enjoys sports. In his spare time he plays football, but also likes running, climbing and basketball. When not working with software or playing sports, he likes to play video games.

Let's turn your idea into reality

Save money, time and energy and book the entire team today.