Sitemap
Kalanasampath

Kalanasampath

Building Maintainable Android Apps with MVVM Architecture in Kotlin

7 min readAug 12, 2026

--

Introduction

Android applications can become difficult to maintain when user interface code, data access, and application logic are written in the same Activity, Fragment, or Composable. This may work for a small application, but as more screens and features are added, the code becomes harder to test, modify, and understand.

One common solution is the MVVM architecture pattern, which stands for Model–View–ViewModel. MVVM separates an Android application into components with different responsibilities. Instead of allowing the user interface to communicate directly with databases, APIs, or other data sources, a ViewModel acts as the connection between the UI and application data.

Modern Android architecture guidance recommends separating applications into at least a UI layer and a data layer, while using an optional domain layer for more complex or reusable business logic. Android also recommends ViewModel as a screen-level state holder.

This article explains how MVVM works in a Kotlin Android application and demonstrates a simple implementation using ViewModel, Repository, StateFlow, Kotlin coroutines, and Jetpack Compose.

Press enter or click to view image in full size

Background: What Is MVVM?

MVVM contains three main components:

Model

The Model represents the application’s data and data-related operations. In a modern Android application, this responsibility is commonly handled through repositories and data sources.

For example, an application may receive data from:

  • A REST API
  • A Room database
  • SharedPreferences or DataStore
  • Local files
  • Other remote services

Android’s recommended architecture places repositories inside the data layer. A repository can coordinate one or more data sources and expose application data to the rest of the app.

View

The View is the part of the application visible to the user.

It can be implemented using:

  • Jetpack Compose
  • Activities
  • Fragments
  • Traditional XML layouts

The View should mainly display state and send user actions, such as button clicks, back to the ViewModel. It should contain as little business logic as possible.

ViewModel

The ViewModel sits between the UI and the application’s data or domain layer.

Its responsibilities usually include:

  • Requesting data
  • Processing user actions
  • Creating UI state
  • Handling screen-level business logic
  • Exposing state to the UI

Android’s ViewModel is designed as a screen-level state holder and can retain its state-processing logic during configuration changes such as Activity recreation.

A simplified MVVM flow is:

View → ViewModel → Repository → Data Source

Updated data then travels back in the opposite direction:

Data Source → Repository → ViewModel → View

This creates a predictable separation between the UI and data-handling code.

Press enter or click to view image in full size

Technical Explanation

Consider a simple Android application that displays a user’s profile.

1. Creating the Model

First, we can represent a user using a Kotlin data class.

data class User(
val id: Int,
val name: String,
val email: String
)

This class only describes the data. It does not contain UI code.

2. Creating the Repository

Next, we create a repository.

class UserRepository {
    suspend fun getUser(): User {
delay(1000)
return User(
id = 1,
name = "Nimal Perera",
email = "nimal@example.com"
)
}
}

The example uses delay() to represent a database or network operation.

In a real project, UserRepository could retrieve information from Retrofit, Room, Firebase, or another data source.

Using a repository prevents the ViewModel from needing to know exactly where the data comes from. Android’s architecture recommendations encourage a clearly defined data layer based around repositories.

3. Defining UI State

Instead of providing many separate variables to the View, we can describe the complete screen using one UI-state class.

data class UserUiState(
val isLoading: Boolean = false,
val user: User? = null,
val errorMessage: String? = null
)

The screen can now be in states such as loading, successfully loaded, or failed.

Keeping related screen information together makes the application’s state easier to understand.

4. Creating the ViewModel

Now we create the ViewModel.

class UserViewModel(
private val repository: UserRepository
) : ViewModel() {
    private val _uiState = MutableStateFlow(UserUiState())    val uiState: StateFlow<UserUiState> =
_uiState.asStateFlow()
fun loadUser() { viewModelScope.launch { _uiState.value =
UserUiState(isLoading = true)
try { val user = repository.getUser() _uiState.value = UserUiState(
user = user
)
} catch (e: Exception) { _uiState.value = UserUiState(
errorMessage = "Unable to load user"
)
}
}
}
}

Several important concepts are being used here.

MutableStateFlow is private because only the ViewModel should modify the screen state. The UI receives the read-only StateFlow.

StateFlow is designed to represent observable state and always provides its current value to collectors.

The repository is passed through the constructor instead of being created directly inside the ViewModel. This makes the code more loosely coupled and easier to test.

Get Kalanasampath’s stories in your inbox

Join Medium for free to get updates from this writer.

The asynchronous operation runs inside viewModelScope. Kotlin coroutines allow asynchronous operations to be expressed using sequential-looking Kotlin code, while Android provides coroutine support that integrates with lifecycle-aware components.

Connecting the ViewModel to Jetpack Compose

The UI can observe the state produced by the ViewModel.

@Composable
fun UserScreen(
viewModel: UserViewModel
) {
    val state by viewModel.uiState
.collectAsStateWithLifecycle()
Column { when { state.isLoading -> {
CircularProgressIndicator()
}
state.errorMessage != null -> {
Text(state.errorMessage!!)
}
state.user != null -> { Text(
text = state.user.name
)
Text(
text = state.user.email
)
}
}
Button(
onClick = {
viewModel.loadUser()
}
) {
Text("Load User")
}
}
}

The Composable does not know how the user information is downloaded or stored. It only understands the UserUiState supplied by the ViewModel.

When the user presses the button, an event travels from the View to the ViewModel:

Button Click
↓
ViewModel
↓
Repository
↓
Data Source

When new data becomes available:

Data Source
↓
Repository
↓
ViewModel
↓
StateFlow
↓
Compose UI

This is closely related to unidirectional data flow, where state moves toward the UI while user events move toward the state holder. This pattern works naturally with Jetpack Compose.

collectAsStateWithLifecycle() is especially useful because it collects a Flow as Compose state while respecting the Android lifecycle. Android recommends this approach for lifecycle-aware Flow collection in Compose applications.

Practical Real-World Applications

MVVM can be used in almost any Android application.

For example, consider an e-commerce application.

The View may display:

  • Product images
  • Product names
  • Prices
  • Loading indicators
  • Error messages

The ViewModel can manage:

  • Loading products
  • Searching
  • Filtering
  • Adding products to a cart
  • Producing the state displayed on the screen

The Repository may communicate with:

  • A REST API
  • A Room database
  • A local cache

A similar architecture can be used for banking applications, food-delivery apps, social-media apps, educational platforms, and booking systems.

For larger applications, an optional domain layer can also be introduced between the ViewModel and Repository. Android recommends using this layer when business logic becomes complex or needs to be reused by multiple ViewModels.

Advantages of MVVM

Separation of Responsibilities

UI rendering, state management, and data access are placed in separate components. This makes the project easier to understand.

Easier Testing

Because the ViewModel receives its dependencies rather than creating them internally, developers can replace real repositories with fake implementations during unit testing.

Better Maintainability

Changes to the UI usually do not require major changes to the data layer. Similarly, changing an API or database implementation does not necessarily require redesigning the screen.

Configuration Change Handling

A ViewModel can survive Activity recreation caused by configuration changes, reducing the need to reload screen information unnecessarily.

Suitable for Modern Android UI

MVVM works especially well with observable state and Jetpack Compose’s state-driven UI model. Android’s Compose architecture guidance specifically describes the use of state holders, events, and unidirectional data flow.

Limitations of MVVM

MVVM is useful, but it is not automatically the correct solution for every application.

One limitation is additional complexity. A very small app may not need repositories, multiple state classes, use cases, and many architectural layers.

Another problem occurs when developers put too much logic into a ViewModel. This can create very large ViewModel classes that are difficult to maintain.

MVVM also requires developers to understand concepts such as StateFlow, coroutines, dependency injection, state management, and application layering.

Finally, ViewModel should not be treated as permanent storage. Although it survives many configuration-related recreations, developers must still design applications to restore important data when required after process death.

Best Practices

When implementing MVVM in Kotlin, several practices can improve the design.

Keep the UI simple. The View should mainly display state and forward user events instead of containing database or network logic.

Expose immutable state. Keep MutableStateFlow private and expose a StateFlow publicly.

private val _uiState =
MutableStateFlow(UserUiState())
val uiState =
_uiState.asStateFlow()

This prevents external classes from modifying ViewModel state directly.

Use repositories for data access. The ViewModel should communicate with a repository rather than directly accessing APIs or databases. This follows Android’s recommended separation between the UI and data layers.

Use lifecycle-aware state collection. With Jetpack Compose, collectAsStateWithLifecycle() is the recommended Android approach for collecting Flow-based UI state.

Use constructor injection. Dependencies such as repositories should normally be provided to the ViewModel rather than created inside it.

Do not over-engineer small applications. Android describes the domain layer as optional, so an additional layer should be introduced when it actually helps manage complexity or reuse business logic.

Conclusion

MVVM is a practical architecture pattern for developing maintainable Android applications with Kotlin. It separates the user interface, application state, and data operations so that each part of the application has a clearer responsibility.

In a modern Android project, MVVM can be implemented using repositories for the data layer, ViewModel for screen-level state and logic, Kotlin coroutines for asynchronous work, StateFlow for observable UI state, and Jetpack Compose for the interface.

The most important idea is not simply creating classes named ViewModel or Repository. The real purpose is to create clear boundaries between responsibilities. When these boundaries are properly designed, Android applications become easier to understand, test, extend, and maintain.

For developers learning modern Android development, MVVM is therefore an important architecture concept and a useful foundation for building larger Kotlin applications.

References

  1. Android Developers. Guide to App Architecture. Android Developers documentation.
  2. Android Developers. Recommendations for Android Architecture.
  3. Android Developers. ViewModel Overview.
  4. Android Developers. Data Layer.
  5. Android Developers. Compose UI Architecture.
  6. Android Developers. Kotlin Coroutines on Android.
  7. Kotlin Documentation. StateFlow — kotlinx.coroutines.

--

--