Benefits of Android clean architecture for developers

What would you prefer: adding a new feature to a very well working app with awful architecture, or fixing a bug in the well architected, but buggy, Android application? Personally, I would definitely choose the second option. Adding a new feature, even a simple one, can become very laborious in an app, considering all the dependencies from everything in every class. I remember one of my Android projects, where a project manager asked me to add a small feature—something like downloading data and displaying it on a new screen. It was an app written by one of my colleagues who found a new job. The feature should not take more than a half of a working day. I was very optimistic…

After seven hours of investigation into how the app works, what modules there are, and how they communicate with one another, I made some trial implementations of the feature. It was hell. A small change in the data model forced a big change in the login screen. Adding a network request required changes of implementation of almost all screens and the GodOnlyKnowsWhatThisClassDoes class. Button color changes caused weird behavior when saving the data to the database or a total app crash. Halfway through the following day, I told my project manager, “We have two ways to implement the feature. First, I can spend three more days on it and finally will implement it in a very dirty way, and the implementation time of every next feature or bugfix will grow exponentially. Or, I can rewrite the app. This will take me two or three weeks, but we will save time for the future app changes.”” Fortunately, he agreed to the second option. If I ever had doubts why good software architecture in an app (even a very small one) is important, this app dispelled them totally. But which Android architecture pattern should we use to avoid such problems?

Lifehack: In case you don’t want to deal with all these issues, there is always an option to hire Android developers for your project.

In this article, I’d like to show you a clean architecture example in an Android app. The main ideas of this pattern, however, can be adapted to every platform and language. Good architecture should be independent of details like platform, language, database system, input, or output.

Example App

We will create a simple Android app to register our location with the following features:

  • The user can create an account with a name.
  • The user can edit the account name.
  • The user can delete the account.
  • The user can select the active account.
  • The user can save location.
  • The user can see the location list for a user.
  • The user can see a list of users.

Clean Architecture

The layers are the main core of a clean architecture. In our app, we will use three layers: presentation, domain, and model. Each layer should be separated and shouldn’t need to know about other layers. It should exist in its own world and, at most, share a small interface to communicate.

Layer responsibilities:

  • Domain: Contains the business rules of our app. It should provide use cases which reflect the features of our app.
  • Presentation: Presents data to the user and also collects necessary data like the username. This is a kind of input/output.
  • Model: Provides data for our app. It is responsible for obtaining data from external sources and save them to the database, cloud server, etc.

Which layers should know about the others? The simplest way to get the answer is thinking about changes. Let’s take the presentation layer—we will present something to the user. If we change something in presentation, should we also make a change in a model layer? Imagine we have a “User” screen with the user’s name and last location. If we want to present the user’s last two locations instead of only one, our model should not be affected. So, we have the first principle: The presentation layer does not know about the model layer.

And, the opposite—should the model layer know about presentation layer? Again—no, because if we change, e.g., the source of data from a database to a network, it should not change anything in the UI (if you thought about adding a loader here—yes, but we can also have a UI loader when using a database). So the two layers are completely separate. Great!

What about the domain layer? It is the most important one because it contains all the main business logic. This is where we want to process our data before passing it to the model layer or presenting it to the user. It should be independent of any other layer—it does not know anything about the database, the network, or the user interface. As this is the core, other layers will communicate only with this one. Why do we want to have this completely independent? Business rules will probably change less often than the UI designs or something in the database or the network storage. We will communicate with this layer via some provided interfaces. It does not use any concrete model or UI implementation. These are details, and remember—details change. A good architecture is not bound to details.

Enough theory for now. Let’s start coding! This article revolves around the code, so—for better understanding—you should download the code from GitHub and check what is inside. There are three Git tags created—architecture_v1, architecture_v2, and architecture_v3, which correspond to the article’s parts.

App Technology

In the app, I use Kotlin and Dagger 2 for dependency injection. Neither Kotlin nor Dagger 2 is necessary here, but it makes things far easier. You might be surprised that I do not use RxJava (nor RxKotlin), but I didn’t find it usable here, and I do not like using any library only because it is on top and somebody says it is a must. As I said—language and libraries are details, so you can use what you want. Some Android unit test libraries are used as well: JUnit, Robolectric, and Mockito.

Domain

The most important layer in our Android application architecture design is the domain layer. Let’s start with it. This is where our business logic and the interfaces to communicate with other layers will be. The main core is the UseCases, which reflect what the user can do with our app. Let’s prepare an abstraction for them:

abstract class UseCase<out Type, in Params> {

    private var job: Deferred<OneOf<Failure, Type>>? = null

    abstract suspend fun run(params: Params): OneOf<Failure, Type>

    fun execute(params: Params, onResult: (OneOf<Failure, Type>) -> Unit) {
        job?.cancel()
        job = async(CommonPool) { run(params) }
        launch(UI) {
            val result = job!!.await()
            onResult(result)
        }
    }

    open fun cancel() {
        job?.cancel()
    }

    open class NoParams
}

I decided to use Kotlin’s coroutines here. Each UseCase has to implement a run method to provide the data. This method is called on a background thread, and after a result is received, it is delivered on the UI thread. The returned type is OneOf<F, T>—we can return an error or success with data:

sealed class OneOf<out E, out S> {
    data class Error<out E>(val error: E) : OneOf<E, Nothing>()
    data class Success<out S>(val data: S) : OneOf<Nothing, S>()

    val isSuccess get() = this is Success<S>
    val isError get() = this is Error<E>

    fun <E> error(error: E) = Error(error)
    fun <S> success(data: S) = Success(data)

    fun oneOf(onError: (E) -> Any, onSuccess: (S) -> Any): Any =
            when (this) {
                is Error -> onError(error)
                is Success -> onSuccess(data)
            }
}

The domain layer needs its own entities, so the next step is to define them. We have two entities for now: User and UserLocation:

data class User(var id: Int? = null, val name: String, var isActive: Boolean = false)

data class UserLocation(var id: Int? = null, val latitude: Double, val longitude: Double, val time: Long, val userId: Int)

Now that we know what data to return, we have to declare our data providers’ interfaces. These will be IUsersRepository and ILocationsRepository. They have to be implemented in the model layer:

interface IUsersRepository {
    fun setActiveUser(userId: Int): OneOf<Failure, User>
    fun getActiveUser(): OneOf<Failure, User?>
    fun createUser(user: User): OneOf<Failure, User>
    fun removeUser(userId: Int): OneOf<Failure, User?>
    fun editUser(user: User): OneOf<Failure, User>
    fun users(): OneOf<Failure, List<User>>
}

interface ILocationsRepository {
    fun locations(userId: Int): OneOf<Failure, List<UserLocation>>
    fun addLocation(location: UserLocation): OneOf<Failure, UserLocation>
}

This set of actions should be enough to provide the necessary data for the app. At this stage, we do not decide how the data will be stored—this is a detail which we want to be independent of. For now, our domain layer doesn’t even know that it’s on Android. We will try to keep this state (Sort of. I’ll explain later).

The last (or almost last) step is to define implementations for our UseCases, which will be used by the presentation data. All of them are very simple (just like our app and data are simple)—their operations are limited to call a proper method from the repository, e.g.:

class GetLocations @Inject constructor(private val repository: ILocationsRepository) : UseCase<List<UserLocation>, UserIdParams>() {
    override suspend fun run(params: UserIdParams): OneOf<Failure, List<UserLocation>> = repository.locations(params.userId)
}

The Repository abstraction makes our UseCases very easy to test — we do not have to care about a network or a database. It can be mocked in any way, so our unit tests will test actual use cases and not other, unrelated classes. This will make our unit tests simple and fast:

@RunWith(MockitoJUnitRunner::class)
class GetLocationsTests {
    private lateinit var getLocations: GetLocations
    private val locations = listOf(UserLocation(1, 1.0, 1.0, 1L, 1))

    @Mock
    private lateinit var locationsRepository: ILocationsRepository

    @Before
    fun setUp() {
        getLocations = GetLocations(locationsRepository)
    }

    @Test
    fun `should call getLocations locations`() {
        runBlocking { getLocations.run(UserIdParams(1)) }
        verify(locationsRepository, times(1)).locations(1)
    }

    @Test
    fun `should return locations obtained from locationsRepository`() {
        given { locationsRepository.locations(1) }.willReturn(OneOf.Success(locations))
        val returnedLocations = runBlocking { getLocations.run(UserIdParams(1)) }
        returnedLocations shouldEqual OneOf.Success(locations)
    }
}

For now, the domain layer is finished.

Model

As an Android developer, you will probably choose Room, the new Android library for storing data. But let’s imagine that the project manager asked if you can put off the decision about the database because management is trying to decide between Room, Realm, and some new, super fast storage library. We need some data to start working with the UI, so we will just keep it in memory for now:

class MemoryLocationsRepository @Inject constructor(): ILocationsRepository {
    private val locations = mutableListOf<UserLocation>()

    override fun locations(userId: Int): OneOf<Failure, List<UserLocation>> = OneOf.Success(locations.filter { it.userId == userId })

    override fun addLocation(location: UserLocation): OneOf<Failure, UserLocation> {
        val addedLocation = location.copy(id = locations.size + 1)
        locations.add(addedLocation)
        return OneOf.Success(addedLocation)
    }
}

Presentation

Two years ago, I wrote an article about MVP as a very good app structure for Android. When Google announced the great Architecture Components, which made Android application development far easier, MVP is no longer needed and can be replaced by MVVM; however, some ideas from this pattern are still very useful—like the one about dumb views. They should only care about displaying the data. To achieve this, we will make use of ViewModel and LiveData.

The design of our app is very simple—one activity with bottom navigation, in which two menu entries show the locations fragment or the users fragment. In these views we use ViewModels, which in turn use UseCases from the domain layer, keeping the communication neat and simple. For example, here is LocationsViewModel:

class LocationsViewModel @Inject constructor(private val getLocations: GetLocations,
                                             private val saveLocation: SaveLocation) : BaseViewModel() {
    var locations = MutableLiveData<List<UserLocation>>()

    fun loadLocations(userId: Int) {
        getLocations.execute(UserIdParams(userId)) { it.oneOf(::handleError, ::handleLocationsChange) }
    }

    fun saveLocation(location: UserLocation, onSaved: (UserLocation) -> Unit) {
        saveLocation.execute(UserLocationParams(location)) {
            it.oneOf(::handleError) { location -> handleLocationSave(location, onSaved) }
        }
    }

    private fun handleLocationSave(location: UserLocation, onSaved: (UserLocation) -> Unit) {
        val currentLocations = locations.value?.toMutableList() ?: mutableListOf()
        currentLocations.add(location)
        this.locations.value = currentLocations
        onSaved(location)
    }

    private fun handleLocationsChange(locations: List<UserLocation>) {
        this.locations.value = locations
    }
}

A little explanation for those who are not familiar with ViewModels—our data is stored in the locations variable. When we obtain data from the getLocations use case, they are passed to the LiveData value. This change will notify the observers so that they can react and update their data. We add an observer for the data in a fragment:

class LocationsFragment : BaseFragment() {

...

    private fun initLocationsViewModel() {
       locationsViewModel = ViewModelProviders.of(activity!!, viewModelFactory)[LocationsViewModel::class.java]
       locationsViewModel.locations.observe(this, Observer<List<UserLocation>> { showLocations(it ?: emptyList()) })
       locationsViewModel.error.observe(this, Observer<Failure> { handleError(it) })
    }

    private fun showLocations(locations: List<UserLocation>) {
        locationsAdapter.locations = locations
    }

    private fun handleError(error: Failure?) {
        toast(R.string.user_fetch_error).show()
    }

}

On every location change, we just pass the new data to an adapter assigned to a recycler view—and that’s where the normal Android flow for showing data in a recycler view goes.

Because we use ViewModel in our views, their behavior is also easy to test—we can just mock the ViewModels and not care about the data source, network, or other factors:

@RunWith(RobolectricTestRunner::class)
@Config(application = TestRegistryRobolectricApplication::class)
class LocationsFragmentTests {

    private var usersViewModel = mock(UsersViewModel::class.java)
    private var locationsViewModel = mock(LocationsViewModel::class.java)

    lateinit var fragment: LocationsFragment

    @Before
    fun setUp() {
        UsersViewModelMock.intializeMock(usersViewModel)
        LocationsViewModelMock.intializeMock(locationsViewModel)

        fragment = LocationsFragment()
        fragment.viewModelFactory = ViewModelUtils.createFactoryForViewModels(usersViewModel, locationsViewModel)
        startFragment(fragment)
    }


    @Test
    fun `should getActiveUser on start`() {
        Mockito.verify(usersViewModel).getActiveUser()
    }

    @Test
    fun `should load locations from active user`() {
        usersViewModel.activeUserId.value = 1
        Mockito.verify(locationsViewModel).loadLocations(1)
    }

    @Test
    fun `should display locations`() {
        val date = Date(1362919080000)//10-03-2013 13:38

        locationsViewModel.locations.value = listOf(UserLocation(1, 1.0, 2.0, date.time, 1))

        val recyclerView = fragment.find<RecyclerView>(R.id.locationsRecyclerView)
        recyclerView.measure(100, 100)
        recyclerView.layout(0,0, 100, 100)
        val adapter = recyclerView.adapter as LocationsListAdapter
        adapter.itemCount `should be` 1
        val viewHolder = recyclerView.findViewHolderForAdapterPosition(0) as LocationsListAdapter.LocationViewHolder
        viewHolder.latitude.text `should equal` "Lat: 1.0"
        viewHolder.longitude.text `should equal` "Lng: 2.0"
        viewHolder.locationDate.text `should equal` "10-03-2013 13:38"
    }
}

You may notice that the presentation layer is also separated into smaller layers with clear borders. Views like activitiesfragmentsViewHolders, etc. are responsible only for displaying data. They are aware only about the ViewModel layer—and use only that to get or to send users and locations. It is a ViewModel which communicates with the domain. ViewModel implementations are the same for the view as the UseCases are for the domain. To paraphrase, clean architecture is like an onion—it has layers, and layers can also have layers.

Dependency Injection

We have created all the classes for our architecture, but there is one more thing to do—we need something that connects everything together. The presentation, domain, and model layers are kept clean, but we need one module which will be the dirty one and will know everything about everything—by this knowledge, it will be able to connect our layers. The best way to make it is using one of the common design patterns (one of the clean code principles defined in SOLID)—dependency injection, which creates proper objects for us and injects them to desired dependencies. I used Dagger 2 here (in the middle of the project, I changed the version to 2.16, which has less boilerplate), but you can use any mechanism you like. Recently, I played a bit with Koin library, and I think it is also worth a try. I wanted to use it here, but I had a lot of problems with mocking the ViewModels when testing. I hope I find a way to resolve them quickly—if so, I can present differences for this app when using Koin and Dagger 2.

You can check the app for this stage on GitHub with the tag architecture_v1.

Changes

We finished our layers, tested the app—everything is working! Except for one thing—we still need to know what database our PM wants to use. Assume they came to you and said that the management agreed to use Room, but they still want to have a possibility to use the newest, superfast library in the future, so you need to keep potential changes in mind. Also, one of the stakeholders asked if the data can be stored in a cloud and wants to know the cost of such a change. So, this is the time to check if our architecture is good and if we can change the data storage system without any changes in the presentation or the domain layer.