Sample Android project: MVVM Clean Architecture with Coroutines + Tips

Fahrizal Sentosa
4 min readFeb 12, 2022

--

Android Template for your next project

Image by hotelintel.co

I want to share how I develop an Android App adapted with MVVM Clean Architecture and another common library such as:
- Coroutines
- Hilt
- Room Database
- Kotlin FLow

MVVM + Clean Architecture

Above is the Clean Architecture flow that shows how the data is requested until data is presented. Clean Architecture divided into 3 layers:

  1. Presentation
  2. Domain
  3. Data

Presentation Layer

The presentation layer is a layer that focuses to handle the UI. Including UI design and also UI logic, that’s it (don’t mix with another logic).

There are two main parts on Presentation Layer: View and ViewModel

View
A view can be an Activity, Fragment, or Custom View.
The Responsibility is displayed from the layout itself or updating UI by the value passed from ViewModel.
The view on MVVM has Unidirectional Data Flow, and it doesn’t have business logic.

private fun fetchPraySchedules() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.uiState.collect { state ->
when (state) {
is HomeViewModel.PrayUiState.Loaded -> onLoaded(state.itemState)
is HomeViewModel.PrayUiState.Error -> showError(state.message)
is HomeViewModel.PrayUiState.Loading -> showLoading()
}
}
}
}
}

As you can see above code, function fetchPraySchedules is invoked to observe uiState. It will be triggered when a state is changed, then updating the UI. When the value is changed or how to change the value is what I mean by UI logic.

Tip
I recommend you to use state flow and the behavior UI depends on a state. Those decrease complexity when communicating between them and easy to read.

ViewModel
The responsibility is to hold data and also as a connector between View and Use Case(domain layer).

Tip
I recommend you to use State Flow + sealed class due to cleaner when defining state and safety.

private val _uiState = MutableStateFlow<PrayUiState>(PrayUiState.Loading)
val uiState: StateFlow<PrayUiState> = _uiState
//...sealed class PrayUiState {
object Loading : PrayUiState()
class Loaded(val itemState: HomeItemUiState) : PrayUiState()
class Error(@StringRes val message: Int) : PrayUiState()
}
  • _uiState (with underscore) modifier private, it avoids modification of a state from view.
  • uiState modifier public, the view is able to access the state but not able to change, making it decouple the data modification.
  • The state type is sealed class: PrayUiState, consisting of 3 states. Each state makes it easy to handle different behavior on the UI and make it safer due to when you add more state to the sealed class without adding the state handler, it will error on the compiler.

Domain Layer

The domain layer is business logic. There’s no knowledge about the view and data. Focus on connecting them. Don’t mix the business logic with UI logic or data repository logic.

Responsibility:
- Connecting between presentation layer and data layer
- Encapsulate Business logic
- Mapping data model to UI model and vice-versa

As you can see, if you don’t implement use case inflict you need to write business logic for each ViewModel, and hard to maintain if you need to change the business logic (you need to change the whole ViewModel). Otherwise, if you implement use case, code is simpler in each ViewModel. The second one, you also encapsulate the business logic and make it easy to maintain as well (you are able to focus business logic on 1 class).

Data Layer

The data layer is a layer focused on managing data. Data is produced on a repository with a responsibility to manage data from the data sources (network, local, cache, or mock).

In this case, I want to get data from a network as efficiently as possible. So, I will invoke data from the network first time only. Afterward, get data from local.

Flowchart of GetPraySchedules

Repository provides function to fetch data, able to invoke from domain layer but domain layer doesn’t need to know where is data coming from. What conditions, when returning data from a network or local, are the responsibility of repository.

Tip
I recommend you use Factory Pattern when you need to implement multiple data sources. We need to provide 2 data sources: network and local

1. Create an interface to vary the entity

interface ScheduleEntityData {

suspend fun getPraySchedule(scheduleRequest: ScheduleRequest): List<Schedule>
}

2. Create class NetworkScheduleEntityData, LocalScheduleEntityData and implement ScheduleEntityData.

3. Implementation Factory Pattern

class ScheduleFactory @Inject constructor(
private val networkScheduleEntityData: NetworkScheduleEntityData,
private val localScheduleEntityData: LocalScheduleEntityData
) {

fun create(source: Source): ScheduleEntityData {
return when (source) {
Source.NETWORK -> networkScheduleEntityData
else -> localScheduleEntityData
}
}
}
class ScheduleRepositoryImpl @Inject constructor(
private val scheduleFactory: ScheduleFactory
) : ScheduleRepository {

override suspend fun getPraySchedules(prayScheduleRequest: PrayScheduleRequest): List<PraySchedule> {
return scheduleFactory.create(Source.LOCAL).getPraySchedule(prayScheduleRequest)
.ifEmpty { syncPraySchedule(prayScheduleRequest) }
}

private suspend fun syncPraySchedule(prayScheduleRequest: PrayScheduleRequest): List<PraySchedule> {
return scheduleFactory.create(Source.NETWORK).getPraySchedule(prayScheduleRequest)
.also { prayScheduleFromNetwork ->
scheduleFactory.create(Source.LOCAL).addPraySchedules(prayScheduleFromNetwork)
}
}
}

As you can see, object scheduleFactory is created based on a param data source. When a source is Network, it will create NetworkScheduleEntityData and vice-versa.

I think that’s all for my explanation and tip about MVVM Clean Architecture.

To see the complete code please see my project on GitHub below!

Thank you for reading!
-FRZ-

--

--