๐Ÿ›๏ธ Android Architecture and Clean Code

MVVM Architecture in Android Explained

MVVM is the default architecture of modern Android apps, recommended in spirit by Google's architecture guide and supported by Jetpack's ViewModel. This guide explains each role precisely, builds a complete feature, and lists the anti-patterns that quietly undermine it.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. The three roles
  2. A complete feature: weather screen
  3. Why the ViewModel matters on Android
  4. Testing the ViewModel
  5. MVVM in different apps
  6. Anti-patterns
  7. Frequently asked questions
  8. Conclusion

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

MVVM roles and data flow on Android The View (Compose screen) observes UI state from the ViewModel and calls ViewModel functions on user actions. The ViewModel holds and transforms UI state and calls the Model layer, repositories, which fetch and store data from network and database. ViewCompose screen ViewModelStateFlow<UiState> Modelrepositories, data events state calls data / Flow
The View never talks to the Model directly, and the ViewModel never references the View.
RoleResponsibilityMust NOT
ViewRender state; forward user actionsContain business logic, fetch data, hold important state
ViewModelHold screen state; apply presentation logic; call the data layer; survive configuration changesReference Activity, Fragment, View or Context; know about Compose specifics
ModelData and business rules: repositories, data sources, optionally use casesKnow which screen uses it

A complete feature: weather screen

Model: WeatherRepository.ktKotlin
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
}
ViewModel: WeatherViewModel.ktKotlin
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.")
            }
        }
    }
}
View: WeatherScreen.ktKotlin
@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.
  • viewModelScope cancels work when the screen is gone for good.
  • SavedStateHandle restores small state after process death.

Testing the ViewModel

WeatherViewModelTest.ktKotlin
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 Color or TextUnit, 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.

See all our apps โ†’

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.

Share: LinkedIn X / Twitter WhatsApp