On this page
Before architecture guidance existed, many Android apps put everything in Activities: network calls, database queries, validation and UI updates in one 2,000-line class that crashed on rotation and couldn't be tested. MVVM splits those responsibilities so each part has one job and can change independently.
The three roles
| Role | Responsibility | Must NOT |
|---|---|---|
| View | Render state; forward user actions | Contain business logic, fetch data, hold important state |
| ViewModel | Hold screen state; apply presentation logic; call the data layer; survive configuration changes | Reference Activity, Fragment, View or Context; know about Compose specifics |
| Model | Data and business rules: repositories, data sources, optionally use cases | Know which screen uses it |
A complete feature: weather screen
data class Forecast(val city: String, val tempC: Int, val condition: String, val fetchedAt: Long)
interface WeatherRepository {
suspend fun getForecast(city: String): Forecast // throws IOException on network failure
}sealed interface WeatherUiState {
data object Idle : WeatherUiState
data object Loading : WeatherUiState
data class Success(val title: String, val temperature: String, val condition: String) : WeatherUiState
data class Error(val message: String) : WeatherUiState
}
@HiltViewModel
class WeatherViewModel @Inject constructor(
private val repository: WeatherRepository,
private val savedState: SavedStateHandle,
) : ViewModel() {
val city: StateFlow<String> = savedState.getStateFlow("city", "Chandigarh")
private val _uiState = MutableStateFlow<WeatherUiState>(WeatherUiState.Idle)
val uiState: StateFlow<WeatherUiState> = _uiState.asStateFlow()
init { refresh() }
fun onCityChange(value: String) { savedState["city"] = value }
fun refresh() {
val target = city.value.trim()
if (target.isEmpty()) return
viewModelScope.launch {
_uiState.value = WeatherUiState.Loading
_uiState.value = try {
val f = repository.getForecast(target)
// Presentation logic lives here: formatting for display
WeatherUiState.Success(title = f.city, temperature = "${f.tempC}ยฐC", condition = f.condition)
} catch (e: IOException) {
WeatherUiState.Error("Couldn't load weather. Check your connection.")
}
}
}
}@Composable
fun WeatherRoute(viewModel: WeatherViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val city by viewModel.city.collectAsStateWithLifecycle()
WeatherScreen(state, city, viewModel::onCityChange, viewModel::refresh)
}
@Composable
fun WeatherScreen(state: WeatherUiState, city: String, onCityChange: (String) -> Unit, onRefresh: () -> Unit) {
Column(Modifier.padding(16.dp)) {
OutlinedTextField(city, onCityChange, label = { Text("City") }, singleLine = true)
Button(onClick = onRefresh, Modifier.padding(vertical = 8.dp)) { Text("Get weather") }
when (state) {
WeatherUiState.Idle -> Unit
WeatherUiState.Loading -> CircularProgressIndicator()
is WeatherUiState.Success -> Column {
Text(state.title, style = MaterialTheme.typography.headlineSmall)
Text(state.temperature, style = MaterialTheme.typography.displayMedium)
Text(state.condition)
}
is WeatherUiState.Error -> Text(state.message, color = MaterialTheme.colorScheme.error)
}
}
}Why the ViewModel matters on Android
- Survives configuration changes: rotation recreates the Activity, but the same ViewModel instance and its in-flight coroutines continue.
viewModelScopecancels work when the screen is gone for good.SavedStateHandlerestores small state after process death.
Testing the ViewModel
class WeatherViewModelTest {
@get:Rule val mainRule = MainDispatcherRule()
private class FakeRepo(var fail: Boolean = false) : WeatherRepository {
override suspend fun getForecast(city: String) =
if (fail) throw IOException() else Forecast(city, 31, "Sunny", 0)
}
@Test fun `success formats temperature`() = runTest {
val vm = WeatherViewModel(FakeRepo(), SavedStateHandle())
advanceUntilIdle()
assertEquals("31ยฐC", (vm.uiState.value as WeatherUiState.Success).temperature)
}
@Test fun `network failure shows error`() = runTest {
val vm = WeatherViewModel(FakeRepo(fail = true), SavedStateHandle())
advanceUntilIdle()
assertTrue(vm.uiState.value is WeatherUiState.Error)
}
}MVVM in different apps
CRUD and content apps
MVVM with one UiState per screen is usually all the architecture you need.
Complex interactive screens
Keep MVVM but formalise state transitions, moving toward MVI.
Large codebases
MVVM in the UI layer, plus a domain layer and modules underneath. See Clean Architecture.
Anti-patterns
- God ViewModel: one ViewModel for many screens, or 1,000 lines of logic. Split by screen and move business rules down.
- Context in ViewModel: leaks. Need strings? Expose resource IDs or use a resource provider abstraction.
- Multiple mutable state streams that can contradict each other.
- View pulls data: composables calling repositories directly.
- ViewModel imports Compose types such as
ColororTextUnit, which couples logic to the UI toolkit.
Frequently asked questions
Is MVVM required by Google?
Google's guide recommends a UI layer with state holders (typically ViewModels), unidirectional data flow and a data layer. That matches MVVM, without mandating the name.
One ViewModel per screen or per composable?
Generally one per screen (navigation destination). Small components get state via parameters.
LiveData or StateFlow?
StateFlow is the common choice for new Kotlin code. LiveData remains supported, especially in View-based apps.
Conclusion
MVVM on Android means Views render state and forward events, ViewModels own screen state and presentation logic, and the Model layer owns data and business rules. Keep Context out of ViewModels, expose a single immutable UiState, and test ViewModels with fakes.
Next step: implement the Model side properly with the Repository Pattern.
Official references
๐ฑ Apps built by Yunok Tech
Real Android apps on Google Play, free to download.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
Ready to build real Android applications?
Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.
Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.
Explore the Android course and take your next step as an Android developer.