Android clean architecture with ViewModel, UseCases and Repositories: Part 1
The last Google I/O the new architecture components for Android were presented. Finally Google showed the Android Developers a way to develop our applications in a clean and reactive way. In this post I’m going to show how this components can be used in an application with a MVVM architecture and, of course, in Kotlin.
I love too much the clean code. There is nothing better than a decoupled, testable and maintainable code. And this is my vision of how to accomplish that, there are another better ways, for sure, and I can’t wait to read them on the comments :)
The application I have developed is quite simple: it has one screen with a paginated RecyclerView that shows a list of cryptocurrencies using the CoinMarketCap api. You can check it on the github repo I created.
First of all, I would like to show how I made the packages structure of the project:
- I have the presentation layer with the Activities, Fragments and ViewModels.
- The domain layer with the Use Cases
- And the model layer with the Repositories.
- Also, I left packages for the dependency injection using Dagger 2 for Android and another with a common files that are mostly the extension functions that we can create with Kotlin.
I’m not going to explain Kotlin specific stuff, if you want to read about it there are another posts around on the internet. Maybe I could write about this topic on another post.
I will explain it starting from bottom layer to the top one. And in the part 2 I will show how easy is to add some unit tests to our architecture.
Model layer
The model layer has all the repositories that the domain layer can use. It has the interface named CoinMarketCapRepository with the method getCryptoList. I am using RxKotlin2 (who has not heard about Rx yet, right?). Otherwise, I’m not going to talk about this topic in this post. Again, you have lot of posts about Rx2 around on the internet :)
So, this Repository is an interface that has to be implemented by someone, this guy is the CoinMarketCapDownloader. We do this way, to make possible the unit tests and to set a contract that the implementation must satisfy.
Basically, the method getCryptoList returns an Rx Single with a List of Crypto model:
data class Crypto( @SerializedName("id") val id: String, @SerializedName("name") val name: String, @SerializedName("symbol") val symbol: String, @SerializedName("rank") val rank: Int, @SerializedName("price_usd") val priceUsd: String, @SerializedName("price_btc") val priceBtc: String, @SerializedName("24h_volume_usd") val twentyFourHvolumeUsd: String, @SerializedName("market_cap_usd") val marketCapUsd: String, @SerializedName("available_supply") val availableSupply: String, @SerializedName("total_supply") val totalSupply: String, @SerializedName("percent_change_1h") val percentChange1h: String, @SerializedName("percent_change_24h") val percentChange24h: String, @SerializedName("percent_change_7d") val percentChange7d: String, @SerializedName("last_updated") val lastUpdated: String )
I provide an object of the class CoinMarketCapApi throught the constructor of the CoinMarketCapDownloader class. The CoinMarketCapApi class has the API contract and allow to make the requests to it. I am using Dagger 2 for Android to achieve the dependency injection, you can read this post to know how to implement the DI, because if you don’t know how to do it yet, you must!
Domain layer
The domain layer contains all the use cases of the application. For now, it has the class CryptoListUseCases (the interface) and CryptoListInteractor (the implementation) with a single method getCryptoListBy which receive the page number we want to request.
The purpose of the Use Cases is to request data to repositories and turn into something usable for the View. Because of that, we are mapping the Crypto model from the Repository to a new view model named CryptoViewModel (I am not very creative 😅). The CryptoListInteractor receives an instance of CoinMarketCapRepository through the constructor, again, using dependency injection.
Presentation layer
Following the MVVM (Model View ViewModel) architecture, at the presentation layer I created MainActivity and the CryptoListFragment (as the View), the CryptoListViewModel (as ViewModel, obviously), and the CryptoListState (as the Model).
Model
The CryptoListState is a model class as follows:
The state of the View is a Kotlin sealed class with his members as abstract members. It has 4 inheritances data classes, one for each state possible in the View: DefaultState, LoadingState, PaginatingState and ErrorState. Simple, right? Each inherited class must override the members from the parent class. They are “must” parameters for the view, so its necessary to receive by the constructor to make an instance. Also, the ErrorState data class has an extra parameter: the errorMessage string. Thereby, we could tell the user if anything goes wrong.
ViewModel
The CryptoListViewModel class:
I think that this class is long enough to explain it separately:
class CryptoListViewModel
@Inject constructor(
private val cryptoListUseCases: CryptoListUseCases,
@Named(SCHEDULER_IO) val subscribeOnScheduler:Scheduler,
@Named(SCHEDULER_MAIN_THREAD) val observeOnScheduler: Scheduler
) : ViewModel()
This is the constructor of this ViewModel, we inject his dependencies using Dagger 2 (you can follow the previous link about dagger to know how to inject dependencies on ViewModels). It needs a CryptoListUseCases instance, and the both Schedulers to subscribe and observe the Rx Single from his method getCryptoListBy(page:Integer).
val stateLiveData = MutableLiveData<CryptoListState>() init {
stateLiveData.value = DefaultState(0, false, emptyList()) }
We set the stateLiveData as a member of the ViewModel class and we instantiate it as a MutableLiveData to allow changes on it. We initialize the value as a DefaultState.
The LiveData is one of the components added to the Android Framework in the architecture components. Basically it is an observer data holder to which we can subscribe so we can receive the changes on it from the View. We will see in a while how to do it. Also, it is a component that survives to the rotation of the device, I mean, it is not destroyed and recreated when the View is destroyed due to a configuration change. However, we have to take into account that it will be destroyed if the Activity is finished or the device needs resources because, for instance, the user opened another application.
private fun getCryptoList(page:Int) {
cryptoListUseCases.getCryptoListBy(page)
.subscribeOn(subscribeOnScheduler)
.observeOn(observeOnScheduler)
.subscribe(this::onCryptoListReceived, this::onError) } private fun onCryptoListReceived(cryptoList: List<CryptoViewModel>) {
val currentCryptoList = obtainCurrentData().toMutableList()
val currentPageNum = obtainCurrentPageNum() + 1
val areAllItemsLoaded = cryptoList.size < LIMIT_CRYPTO_LIST
currentCryptoList.addAll(cryptoList)
stateLiveData.value = DefaultState(
currentPageNum, areAllItemsLoaded, currentCryptoList
)
} private fun onError(error: Throwable) {
val pageNum = stateLiveData.value?.pageNum ?: 0
stateLiveData.value = ErrorState(
error.message ?: "",
pageNum,
obtainCurrentLoadedAllItems(),
obtainCurrentData()
)
}private fun obtainCurrentPageNum() =
stateLiveData.value?.pageNum ?: 0 private fun obtainCurrentData() =
stateLiveData.value?.data ?: emptyList() private fun obtainCurrentLoadedAllItems() =
stateLiveData.value?.loadedAllItems ?: false
The class private methods has the responsability to request the crypto list by the page that the client set. If the request goes well, we set the stateLiveData value to the DefaultState with the new list. Otherwise, if the request goes wrong, we set it to the ErrorState. As I said, setting the LiveData value to a new one we will inform all the subscribers (in our case, the view) that the value has changed.
Finally, the public methods:
fun updateCryptoList() {
val pageNum = obtainCurrentPageNum()
stateLiveData.value = if (pageNum == 0)
LoadingState(pageNum, false, obtainCurrentData())
else
PaginatingState(pageNum, false, obtainCurrentData())
getCryptoList(pageNum)
}fun resetCryptoList() {
val pageNum = 0
stateLiveData.value = LoadingState(pageNum, false, emptyList())
updateCryptoList()
}fun restoreCryptoList() {
val pageNum = obtainCurrentPageNum()
stateLiveData.value = DefaultState(
pageNum, false, obtainCurrentData())
}
This methods look quite simple, aren’t them? We set the state member as LoadingState or PaginatingState depending on the pageNum the client requested.
View
I only show how I developed the Fragment, because the Activity has no mystery at all.
Here, I’m going to explain the funny parts: the ones related with the ViewModel.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(CryptoListViewModel::class.java) observeViewModel() savedInstanceState?.let {
viewModel.restoreCryptoList()
} ?: viewModel.updateCryptoList()
}
In the onCreate method, I obtain the CryptoListViewModel using a custom factory for Dagger 2, you can check it on my repo but the merit is for Alex Facciorusso and his awsome post that translate the needed class to Kotlin.
Also, we start to observe the changes on the stateLiveData of the ViewModel, accessing to it (it is a public member, however you can set it as private and allow the access through a getter in the ViewModel if you prefer so) and calling to his observe method.
private fun observeViewModel() =
viewModel.stateLiveData.observe(this, stateObserver)
This method, has to receive an instance of a LifecycleOwner (the Fragment) and the Observer. I created a member for the second one, the stateObserver:
private val stateObserver =
Observer<CryptoListState> {
state -> state?.let {
isLastPage = state.loadedAllItems
when (state) {
is DefaultState -> {
isLoading = false
swipeRefreshLayout.isRefreshing = false
cryptoListAdapter.updateData(it.data
}
is LoadingState -> {
swipeRefreshLayout.isRefreshing = true
isLoading = true
}
is PaginatingState -> {
isLoading = true
}
is ErrorState -> {
isLoading = false
swipeRefreshLayout.isRefreshing = false
cryptoListAdapter.removeLoadingViewFooter()
}
}
}
}
The stateObserver is an state machine that shows the view properly depending on the state. We need the isLoading member in order to make the magic of the PaginationAdapter class works. It is an abstract custom adapter that makes easier the pagination of the RecyclerViews, you can check it on my github repo.
We should not forget to remove the current observer in the onDestroy method…
viewModel.stateLiveData.removeObserver(stateObserver)
…in order to not have observers attached to the ViewModel that are not alive causing possible memory leaks on our application.
Last of all, this lines from onCreate method…
savedInstanceState?.let {
viewModel.restoreCryptoList()
} ?: viewModel.updateCryptoList()
… tell to the ViewModel if the list needs to be restored because a savedInstanceState exists or if we need to request it.
That’s it! In the second part of this series of articles I will explain how I developed the unit tests:
Thanks for reading! And if you enjoyed it, gimme a clap, a star on github or follow me on twitter, it is my first post and you will make me very happy! 😻 😻