Android, MVVM with Clean Code

S A Z I B
5 min readMar 3, 2023

Android application with MVVM, Clean Code architecture, KOIN, Flow, Coroutine

Android Architecture pattern

This is a beginner project with the goal of seeing how MVVM, Clean Code, and KOIN work together. The project is designed for learning various concepts of MVVM and Clean Code architecture, including dependency injection and testability.

The project uses the https://mockapi.io/ API to retrieve the exchange rate, if you want to use a different key (or the one inside the project has expired), take a look at app/build.gradle, you can change the value there.

What’s inside?

In this project you’ll find.

  • MVVM and Clean Code architectural pattern, using views-> view models -> use cases -> repositories
  • Dependency injection using Koin
  • KTOR for networking
  • Coroutines for asynchronous operations
  • Live Data and UI states handling for loading/success/error
  • Flow

Why MVVM with Clean Code?

MVVM is a design pattern that has become popular in Android development. The benefit of MVVM is to keep the view and business logic separate. Clean Code can help you separate these two concerns, while keeping your project clean, solid, and easy to maintain. A disadvantage of MVVM is that in large projects the View Model is overloaded with business logic, in such cases MVVM with the Clean Code principles can help to separate responsibilities and keep the project solid and easy to evolve and maintain.

Who is this project for?

  • Anyone looking for a way to structure an android application that is easy to evolve and maintain
  • A quick reference for a MVVM with Clean architecture
  • It is neither a UI/Material Design nor a jetpack compose sample.

The project Structure

The project is divided into three layers:

  • Presentation
  • Domain
  • Data

Presentation Layer

The presentation layer is the user-facing layer of the application and is responsible for collecting input from the user and displaying the results of their actions. It is typically composed of a graphical user interface, such as a web page, a mobile app, or a desktop application. The presentation layer contains the view and viewmodel objects from MVVM. The views are Activities or Fragments, they must be as simple as possible, if you have any business logic inside the views you need to refactor these classes.

This is a simpler version to show the fragment’s responsibilities:


class HomeActivity : AppCompatActivity() {

private lateinit var binding: ActivityHomeBinding
private lateinit var productAdapter: ProductListAdapter
private lateinit var layoutManager: LayoutManager

private val viewModel: HomeViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
WindowCompat.setDecorFitsSystemWindows(window, false)
super.onCreate(savedInstanceState)

binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)

productAdapter = ProductListAdapter()
layoutManager = LinearLayoutManager(this@HomeActivity)

binding.rvProduct.apply {
layoutManager = this@HomeActivity.layoutManager
adapter = productAdapter
}

viewModel.homeDataList.observe(this) { response_ ->
response_?.let {
productAdapter.addDataToList(it)
}
Log.d("home_screen", "$response_")
}
}
}

View Model has the following features:

— It is responsible for holding and managing UI-related data in a lifecycle conscious way.

— It is designed to store and manage UI-related data so that the data survives configuration changes such as screen rotations.

— It allows data to survive process death, such as when the user switches from the app or when the device goes to sleep.

— It is a good practice to use the View Model class when you want to separate the UI controller logic from the business logic.

— It is also useful in implementing the MVVM (Model-View-ViewModel) pattern, which helps to separate the presentation layer from the business logic.

— It allows for easier unit testing, since the View Model class can be tested without the UI.

In an Android application the ViewModel can be implemented using the ViewModel Jetpack Library, in our sample app the ViewModel uses a State pattern, an object responsible to hold a value with its UI state (loading, success, error.)

This is simpler version of app ViewModel and State classes:


class HomeViewModel(
private val repository: HomeRepository,
private val sessionPrefs: Prefs
) : ViewModel() {

val homeDataList = MutableLiveData<List<Result>?>()
val userDataList = MutableLiveData<State<List<Result>>>()

init {
// getHomeData()
getUserdata()
}

private fun getUserdata() {
viewModelScope.launch {

repository.getUserListData().collect {
when (it) {
is ResponseResource.Error -> {
Log.d("HomeViewModel", "Some error happened ${it.errorMessage}")
}
is ResponseResource.Success -> {
userDataList.postValue(
State.success(
msg = APP_SUCCESS_MESSAGE,
data = null /*add your data*/
)
)
Log.d("HomeViewModel", "${it.data}")
}
}
}
}
}

private fun getHomeData() {
viewModelScope.launch {

repository.getHomeData().collect {
when (it) {
is ResponseResource.Error -> {
Log.d("HomeViewModel", "Some error happened ${it.errorMessage}")
}
is ResponseResource.Success -> {
homeDataList.postValue(it.data.results)
Log.d("HomeViewModel", "${it.data}")
}
}
}
}
}
}

HomeViewModel.kt


data class State<out T>(
val status: Status,
val data: T?,
val message: String?
) {

companion object {

fun <T> success(msg: String?, data: T?): State<T> = State(SUCCESS, data, msg)
fun <T> loading(data: T?): State<T> = State(LOADING, data, null)
fun <T> error(msg: String, data: T?): State<T> = State(ERROR, data, msg)
}
}

enum class Status {
SUCCESS,
ERROR,
LOADING
}

State class

The Domain Layer

The domain layer of clean architecture is the innermost layer and the core of the application. It contains the application’s business logic and rules and is independent of any other layer. It contains the Entities (domain objects), Use Cases (interactors) and the Repositories (interfaces to the outside world). This layer is isolated from the outer layers, such as the data and presentation layers, as it is not aware of any implementation details. It is solely responsible for the application’s business rules and logic, and is the only layer that is allowed to directly access the database.

Domain Layer: Use case, Model

Here’s a simpler version of Auth Use-Cases class:


class AuthUseCase() {

fun authUseCaseExample(): Flow<ResponseResource<ExampleDto>> {

//some logic
//return data
}
}

The Data Layer

The Data Layer of clean architecture is responsible for managing the data flow and fetching data from different sources such as databases, web services, files, etc. It is also responsible for persisting data and providing access to the data. The Data Layer also provides an interface for the other layers of the application to interact with the data. It also provides an abstraction layer between the data and the other layers of the application.


interface HomeRepository {
suspend fun getHomeData(): Flow<ResponseResource<DummyResponse2>>
suspend fun getUserListData(): Flow<ResponseResource<ArrayList<DummyUserResponse>>>
}


class HomeRepositoryImpl(
private val remote: HomeRemote,
private val sessionPrefs: Prefs
) : HomeRepository {

override suspend fun getHomeData(): Flow<ResponseResource<DummyResponse2>> =
flow {
val responseResource = when (val response = remote.getHomeList()) {
is ResponseResource.Error -> ResponseResource.error(response.errorMessage)
is ResponseResource.Success -> ResponseResource.success(response.data)
}
emit(responseResource)
}

override suspend fun getUserListData(): Flow<ResponseResource<ArrayList<DummyUserResponse>>> =
flow {
val responseResource = when (val response = remote.getUserList()) {
is ResponseResource.Error -> ResponseResource.error(response.errorMessage)
is ResponseResource.Success -> ResponseResource.success(response.data)
}
emit(responseResource)
}
}

Dependency Injection

please take a look here. In the app, Koin is used to inject UseCases, Repositories, and Dao, Viewmodel, Preference etc. Take a look at di package to understand how the objects are provided.

KOIN

The source code

Here is the source code

The app in action:

Ping me: Linkedin

--

--