ViewModels in Clean Architecture — Dos and Don’ts — Part 1

BHAVNA THACKER
5 min readMar 11, 2023

If you have been developing Android apps for sometime now, then ViewModel is your buddy even before Jetpack Compose OR even Kotlin came into picture.

ViewModels in Clean Architecture — Dos and Don’ts — Part 1

In this blog series,

  • You will get on overview of some of the best practices and common mistakes to avoid while working with the ViewModel.
  • You will also see with examples as to when there is a need for refactoring the ViewModel in order to comply with Clean Architecture.

As this topic is going to be lengthy, I prefer splitting it into parts with this being the first one.

Let’s get started.

ViewModel Responsibilities

First of all, understand where does ViewModel stand in the Guide to Android App Architecture. Understanding ViewModel’s Role and Responsibility clearly helps you understand — what it should and should not do.

Source: https://developer.android.com/topic/architecture
Source: https://developer.android.com/topic/architecture

ViewModel is essentially the part of UI layer — wherein UI elements are provided by Activity/Fragment/Composables and ViewModel acts as a State Holder. You can implement a state holder either through a ViewModel or a plain class. Details, refer here.

The UI layer guide recommends unidirectional data flow (UDF) as a means of producing and managing the UI State for the UI layer.

Common Mistake 1 — Passing all User events to ViewModel

After understanding above, a common mistake by developers is passing every event from UI elements to ViewModel — irrespective of whether it requires a business logic OR no.

Source: https://developer.android.com/topic/architecture/ui-layer/events#decision-tree

Above diagram clearly depicts that for pure UI behavior logic, you may not pass the event to the ViewModel.

Let’s understand this with a simple example — A screen with two buttons — Collapse/Expand and Refresh Data.

The UI can handle user events directly if those events relate to modifying the state of a UI element — for example, the state of an expandable item. If the event requires performing business logic, such as refreshing the data on the screen, it should be processed by the ViewModel.

@Composable
fun LatestNewsScreen(viewModel: LatestNewsViewModel = viewModel()) {

// State of whether more details should be shown
var expanded by remember { mutableStateOf(false) }

Column {
Text("Some text")
if (expanded) {
Text("More details")
}

Button(
// The expand details event is processed by the UI that
// modifies this composable's internal state.
onClick = { expanded = !expanded }
) {
val expandText = if (expanded) "Collapse" else "Expand"
Text("$expandText details")
}

// The refresh event is processed by the ViewModel that is in charge
// of the UI's business logic.
Button(onClick = { viewModel.refreshNews() }) {
Text("Refresh data")
}
}
}

Common Mistake 2 —Having information about UI elements in ViewModel

Now that you know that an event that requires business logic, it should be passed to the ViewModel — so that ViewModel takes care of passing it to Data Layer via Domain layer.

A classic example of this is a User clicking a Login Button. Suppose for this case, you have a LoginViewModeland LoginUiState.

Business requirement is to show a SnackBar to the user with the message when Login fails.

A common mistake is to Design LoginUiState as below:

data class LoginUiState(
val snackBarMessage: String? = null,
val isUserLoggedIn: Boolean = false
)

If tomorrow, UI/UX team decides to show an alert dialog to the user instead of SnackBar on login fail. What will you do? Would you still use snackBarMessage? OR refactor it to alertDialogMessage?

Solution to this is that ViewModel should only take care of holding the latest state, updating the state when event occurs and informing the UI about the updated state. It should not have any information about what UI elements are used and this information should not be part of UiState as well. So, Design LoginUiState as below:

data class LoginUiState(
val userMessage: String? = null,
val isUserLoggedIn: Boolean = false
)

Now, let UI (Activity/Fragment/Composable) observe the state and decide what to show and when this what to show changes, change doesn’t involve any change in ViewModel.

Refactor Code?

So, now if you see properties in your UiState data class like snackBarMessage, alertDialogMessage OR functions in your ViewModel like onDialogShownOR onSnackBarShown — You would rather refactor it to errorMessage/userMessage and onErrorMessageShown/onUserMessageShown etc.

Common Mistake 3 — Injecting any data source (Local — Room Dao, SharedPreferences, DataStore OR Remote API etc.) in ViewModel in hurry

  • In two layer architecture — ViewModel interacts with Data layer via Repositories
  • In three layer architecture — ViewModel interacts with Domain layer via UseCases

In any case, ViewModel should not interact with any data sources directly because it is recommended that Data Sources should only be accessed via Repositories.

In order to strictly follow this, better would be to have Dependency Injection via Hilt in your app so that all dependencies and any violation can be caught at a glance.

Refactor Code?

If you see any of the below, it is time to refactor.

@HiltViewModel
class NewsViewModel @Inject constructor(
newsApi: NewsApi,
.................,
) : ViewModel() {
@HiltViewModel
class NewsViewModel @Inject constructor(
newsDataStore: NewsDataStore,
.................,
) : ViewModel() {
@HiltViewModel
class NewsViewModel @Inject constructor(
newsDao: NewsDao,
.................,
) : ViewModel() {

How would Clean Approach look like then?

Two Layer Architecture:

@HiltViewModel
class NewsViewModel @Inject constructor(
newsRepository: NewsRepository,
authorsRepository: AuthorsRepository
) : ViewModel() {

Three Layer Architecture:

@HiltViewModel
class NewsViewModel @Inject constructor(
getLatestNewsWithAuthorsUseCase: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
class GetLatestNewsWithAuthorsUseCase @Inject constructor(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }

Hope you like reading this post about best practices around ViewModel! I will come up with next part soon.

Till then, Happy Coding!

--

--

BHAVNA THACKER

Android GDE. Youtuber — LearnAndroid. Senior Android Engineer @MEGA. FPE @raywenderlich.