Better Android Apps Using MVVM with Clean Architecture
If you don’t choose the right architecture for your Android project, you will have a hard time maintaining it as your codebase grows and your team expands.
This isn’t just an Android MVVM tutorial. In this article, we are going to combine MVVM (Model-View-ViewModel or sometimes stylized “the ViewModel pattern”) with Clean Architecture. We are going to see how this architecture can be used to write decoupled, testable, and maintainable code.
MVVM separates your view (i.e. Activity
s and Fragment
s) from your business logic.
MVVM is enough for small projects, but when your codebase becomes huge,
your ViewModel
s start bloating.
Separating responsibilities becomes hard.
MVVM with Clean Architecture is pretty good in such cases. It goes one step further in separating the responsibilities of your code base. It clearly abstracts the logic of the actions that can be performed in your app.
Note: You can combine Clean Architecture with the
model-view-presenter (MVP) architecture as well. But since Android
Architecture Components already provides a built-in ViewModel
class, we are going
with MVVM over MVP—no MVVM framework required!
· Your code is even more easily testable than with plain MVVM.
· Your code is further decoupled (the biggest advantage.)
· The package structure is even easier to navigate.
· The project is even easier to maintain.
· Your team can add new features even more quickly.
· It has a slightly steep learning curve. How all the layers work together may take some time to understand, especially if you are coming from patterns like simple MVVM or MVP.
· It adds a lot of extra classes, so it’s not ideal for low-complexity projects.
Our data flow will look like this:
Our business logic is completely decoupled from our UI. It makes our code very easy to maintain and test.
The example we are going to see is quite simple. It allows users to create new posts and see a list of posts created by them. I’m not using any third-party library (like Dagger, RxJava, etc.) in this example for the sake of simplicity.
The code is divided into three separate layers:
1. Presentation Layer
2. Domain Layer
3. Data Layer
We’ll get into more detail about each layer below. For now, our resulting package structure looks like this:
Even within the Android app architecture we’re using, there are many ways to structure your file/folder hierarchy. I like to group project files based on features. I find it neat and concise. You are free to choose whatever project structure suits you.
This includes our Activity
s, Fragment
s, and ViewModel
s. An Activity
should be as
dumb as possible. Never put your business logic in Activity
s.
An Activity
will talk to
a ViewModel
and a ViewModel
will talk to the domain
layer to perform actions. A ViewModel
never talks to the data
layer directly.
Here we are passing a UseCaseHandler
and two UseCase
s to our ViewModel
. We’ll get into that in more
detail soon, but in this architecture, a UseCase
is an action that
defines how a ViewModel
interacts with the
data layer.
Here’s how our Kotlin code looks:
class PostListViewModel(
val useCaseHandler: UseCaseHandler,
val getPosts: GetPosts,
val savePost: SavePost): ViewModel() {
fun getAllPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) {
val requestValue = GetPosts.RequestValues(userId)
useCaseHandler.execute(getPosts, requestValue,
object :
UseCase.UseCaseCallback<GetPosts.ResponseValue> {
override
fun onSuccess(response: GetPosts.ResponseValue) {
callback.onPostsLoaded(response.posts)
}
override
fun onError(t: Throwable) {
callback.onError(t)
}
})
}
fun savePost(post: Post, callback: PostDataSource.SaveTaskCallback) {
val requestValues = SavePost.RequestValues(post)
useCaseHandler.execute(savePost, requestValues,
object :
UseCase.UseCaseCallback<SavePost.ResponseValue> {
override
fun onSuccess(response: SavePost.ResponseValue) {
callback.onSaveSuccess()
}
override
fun onError(t: Throwable) {
callback.onError(t)
}
})
}
}
The domain layer contains all the use cases of your
application. In this example, we have UseCase
, an abstract class. All
our UseCase
s will extend this
class.
abstract
class UseCase<Q : UseCase.RequestValues, P : UseCase.ResponseValue> {
var requestValues: Q? =
null
var useCaseCallback: UseCaseCallback<P>? =
null
internal
fun run() {
executeUseCase(requestValues)
}
protected
abstract
fun executeUseCase(requestValues: Q?)
/**
* Data passed to a request.
*/
interface RequestValues
/**
* Data received from a request.
*/
interface ResponseValue
interface UseCaseCallback<R> {
fun onSuccess(response: R)
fun onError(t: Throwable)
}
}
And UseCaseHandler
handles
execution of a UseCase
. We should never
block the UI when we fetch data from the database or our remote server. This is
the place where we decide to execute our UseCase
on a background thread
and receive the response on the main thread.
class UseCaseHandler(
private
val mUseCaseScheduler: UseCaseScheduler) {
fun <T : UseCase.RequestValues, R : UseCase.ResponseValue> execute(
useCase: UseCase<T, R>, values: T, callback: UseCase.UseCaseCallback<R>) {
useCase.requestValues = values
useCase.useCaseCallback = UiCallbackWrapper(callback,
this)
mUseCaseScheduler.execute(Runnable {
useCase.run()
})
}
private
fun <V : UseCase.ResponseValue> notifyResponse(response: V,
useCaseCallback: UseCase.UseCaseCallback<V>) {
mUseCaseScheduler.notifyResponse(response, useCaseCallback)
}
private
fun <V : UseCase.ResponseValue> notifyError(
useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) {
mUseCaseScheduler.onError(useCaseCallback, t)
}
private
class UiCallbackWrapper<V : UseCase.ResponseValue>(
private
val mCallback: UseCase.UseCaseCallback<V>,
private
val mUseCaseHandler: UseCaseHandler) : UseCase.UseCaseCallback<V> {
override
fun onSuccess(response: V) {
mUseCaseHandler.notifyResponse(response, mCallback)
}
override
fun onError(t: Throwable) {
mUseCaseHandler.notifyError(mCallback, t)
}
}
companion
object {
private
var INSTANCE: UseCaseHandler? =
null
fun getInstance(): UseCaseHandler {
if (INSTANCE ==
null) {
INSTANCE = UseCaseHandler(UseCaseThreadPoolScheduler())
}
return INSTANCE!!
}
}
}
As its name implies, the GetPosts
UseCase
is responsible for
getting all posts of a user.
class GetPosts(
private
val mDataRepository: PostDataRepository) :
UseCase<GetPosts.RequestValues, GetPosts.ResponseValue>() {
protected
override
fun executeUseCase(requestValues: GetPosts.RequestValues?) {
mDataRepository.getPosts(requestValues?.userId ?:
-1,
object :
PostDataSource.LoadPostsCallback {
override
fun onPostsLoaded(posts: List<Post>) {
val responseValue = ResponseValue(posts)
useCaseCallback?.onSuccess(responseValue)
}
override
fun onError(t: Throwable) {
// Never use generic exceptions. Create proper exceptions. Since
// our use case is different we will go with generic throwable
useCaseCallback?.onError(Throwable(
"Data not found"))
}
})
}
class RequestValues(
val userId:
Int) : UseCase.RequestValues
class ResponseValue(
val posts: List<Post>) : UseCase.ResponseValue
}
The purpose of the UseCase
s is to be a
mediator between your ViewModel
s and Repository
s.
Let’s say in the future you decide to add an “edit post” feature.
All you have to do is add a new EditPost
UseCase
and all its code will
be completely separate and decoupled from other UseCase
s. We’ve all seen it many times: New
features are introduced and they inadvertently break something in preexisting
code. Creating a separate UseCase
helps immensely in
avoiding that.
Of course, you can’t eliminate that possibility 100 percent, but you sure can minimize it. This is what separates Clean Architecture from other patterns: The code is so decoupled that you can treat every layer as a black box.
This has all the repositories which the domain layer can use. This layer exposes a data source API to outside classes:
interface PostDataSource {
interface LoadPostsCallback {
fun onPostsLoaded(posts: List<Post>)
fun onError(t: Throwable)
}
interface SaveTaskCallback {
fun onSaveSuccess()
fun onError(t: Throwable)
}
fun getPosts(userId: Int, callback: LoadPostsCallback)
fun savePost(post: Post)
}
PostDataRepository
implements PostDataSource
. It decides whether we fetch
data from a local database or a remote server.
class PostDataRepository private constructor(
private
val localDataSource: PostDataSource,
private
val remoteDataSource: PostDataSource): PostDataSource {
companion
object {
private
var INSTANCE: PostDataRepository? =
null
fun getInstance(localDataSource: PostDataSource,
remoteDataSource: PostDataSource): PostDataRepository {
if (INSTANCE ==
null) {
INSTANCE = PostDataRepository(localDataSource, remoteDataSource)
}
return INSTANCE!!
}
}
var isCacheDirty =
false
override
fun getPosts(userId: Int, callback: PostDataSource.LoadPostsCallback) {
if (isCacheDirty) {
getPostsFromServer(userId, callback)
}
else {
localDataSource.getPosts(userId,
object : PostDataSource.LoadPostsCallback {
override
fun onPostsLoaded(posts: List<Post>) {
refreshCache()
callback.onPostsLoaded(posts)
}
override
fun onError(t: Throwable) {
getPostsFromServer(userId, callback)
}
})
}
}
override
fun savePost(post: Post) {
localDataSource.savePost(post)
remoteDataSource.savePost(post)
}
private
fun getPostsFromServer(userId: Int, callback: PostDataSource.LoadPostsCallback) {
remoteDataSource.getPosts(userId,
object : PostDataSource.LoadPostsCallback {
override
fun onPostsLoaded(posts: List<Post>) {
refreshCache()
refreshLocalDataSource(posts)
callback.onPostsLoaded(posts)
}
override
fun onError(t: Throwable) {
callback.onError(t)
}
})
}
private
fun refreshLocalDataSource(posts: List<Post>) {
posts.forEach {
localDataSource.savePost(it)
}
}
private
fun refreshCache() {
isCacheDirty =
false
}
}
The code is mostly self-explanatory. This class has two
variables, localDataSource
and remoteDataSource
. Their type is PostDataSource
, so we don’t care how they
are actually implemented under the hood.
In my personal experience, this architecture has proved to be invaluable. In one of my apps, I started with Firebase on the back end which is great for quickly building your app. I knew eventually I’d have to shift to my own server.
When I did, all
I had to do was change the implementation in RemoteDataSource
. I didn’t have to
touch any other class even after such a huge change. That is the
advantage of decoupled code. Changing any given class shouldn’t affect other
parts of your code.
Some of the extra classes we have are:
interface UseCaseScheduler {
fun execute(runnable: Runnable)
fun <V : UseCase.ResponseValue> notifyResponse(response: V,
useCaseCallback: UseCase.UseCaseCallback<V>)
fun <V : UseCase.ResponseValue> onError(
useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable)
}
class UseCaseThreadPoolScheduler : UseCaseScheduler {
val POOL_SIZE =
2
val MAX_POOL_SIZE =
4
val TIMEOUT =
30
private
val mHandler = Handler()
internal
var mThreadPoolExecutor: ThreadPoolExecutor
init {
mThreadPoolExecutor = ThreadPoolExecutor(POOL_SIZE, MAX_POOL_SIZE, TIMEOUT.toLong(),
TimeUnit.SECONDS, ArrayBlockingQueue(POOL_SIZE))
}
override
fun execute(runnable: Runnable) {
mThreadPoolExecutor.execute(runnable)
}
override
fun <V : UseCase.ResponseValue> notifyResponse(response: V,
useCaseCallback: UseCase.UseCaseCallback<V>) {
mHandler.post { useCaseCallback.onSuccess(response) }
}
override
fun <V : UseCase.ResponseValue> onError(
useCaseCallback: UseCase.UseCaseCallback<V>, t: Throwable) {
mHandler.post { useCaseCallback.onError(t) }
}
}
UseCaseThreadPoolScheduler
is
responsible for executing tasks asynchronously using ThreadPoolExecuter
.
class ViewModelFactory : ViewModelProvider.Factory {
override
fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass == PostListViewModel::
class.java) {
return PostListViewModel(
Injection.provideUseCaseHandler()
, Injection.provideGetPosts(), Injection.provideSavePost())
as T
}
throw IllegalArgumentException(
"unknown model class $modelClass")
}
companion
object {
private
var INSTANCE: ViewModelFactory? =
null
fun getInstance(): ViewModelFactory {
if (INSTANCE ==
null) {
INSTANCE = ViewModelFactory()
}
return INSTANCE!!
}
}
}
This is our ViewModelFactory
. You have to create this to pass arguments in
your ViewModel
constructor.
I’ll explain dependency injection with an example. If you look at
our PostDataRepository
class, it
has two dependencies, LocalDataSource
and RemoteDataSource
. We use the Injection
class to provide these
dependencies to the PostDataRepository
class.
Injecting dependency has two main advantages. One is that you get
to control the instantiation of objects from a central place instead of
spreading it across the whole codebase. Another is that this will help us write
unit tests for PostDataRepository
because now
we can just pass mocked versions of LocalDataSource
and RemoteDataSource
to the PostDataRepository
constructor
instead of actual values.
object Injection {
fun providePostDataRepository(): PostDataRepository {
return PostDataRepository.getInstance(provideLocalDataSource(), provideRemoteDataSource())
}
fun provideViewModelFactory() = ViewModelFactory.getInstance()
fun provideLocalDataSource(): PostDataSource = LocalDataSource.getInstance()
fun provideRemoteDataSource(): PostDataSource = RemoteDataSource.getInstance()
fun provideGetPosts() = GetPosts(providePostDataRepository())
fun provideSavePost() = SavePost(providePostDataRepository())
fun provideUseCaseHandler() = UseCaseHandler.getInstance()
}