android · 3 min read

Android MVVM that stays easy to change

By Vivek Patel · Updated

MVVM separates screen rendering from screen state. On Android, a ViewModel holds state across configuration changes and delegates persistence to repositories. Compose and XML can use the same state owner, which makes UI migration less disruptive.

How the pieces connect

Screen→ ViewModel→ Repository contract→ Data implementation

An illustrative folder structure

This structure explains the architecture. Exact generated paths are shown in the source walkthrough below.

app/src/main/java/com/example/app/
  presentation/home/
    HomeScreen.kt
    HomeViewModel.kt
    HomeUiState.kt
  domain/repository/CounterRepository.kt
  data/repository/MemoryCounterRepository.kt
  di/AppContainer.kt

Expose immutable screen state

Keep MutableStateFlow private and expose StateFlow. Model what the screen needs in a data class instead of exposing several unrelated booleans. A list screen may need content, refresh status, and a recoverable error together. Do not store an Activity or View in the ViewModel.

Collect with the screen lifecycle

Compose screens can collect with collectAsStateWithLifecycle. XML fragments should launch collection with viewLifecycleOwner and repeatOnLifecycle. The view lifecycle is shorter than the fragment lifecycle; binding references must be cleared when the view is destroyed.

Inject repositories, not concrete transports

The ViewModel should receive a repository interface rather than construct Retrofit or Room. Hilt, Koin, or a small manual container can supply that interface. This supports tests that control timing and failure conditions without relying on a server.

Use cases are optional coordination points

Introduce a use case when several repositories or business rules form an operation. Avoid a class per trivial getter. MVVM alone does not enforce clean dependency direction: keep data implementation imports away from presentation if that boundary matters to your team.

Inside the generated starter

Follow a Compose counter through HomeViewModel to the repository. Then replace the in-memory implementation while preserving the screen contract. The same boundary is useful for settings, saved items, or a local draft.

These source files are produced from the same templates used by the generator, with the configuration below. They are starter examples; the observations identify work still needed for a real feature.

app/src/main/java/com/example/myandroidapp/domain/repository/CounterRepository.kt

package com.example.myandroidapp.domain.repository
interface CounterRepository { suspend fun load(): Int; suspend fun save(value: Int) }

app/src/main/java/com/example/myandroidapp/presentation/home/HomeViewModel.kt

package com.example.myandroidapp.presentation.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

import com.example.myandroidapp.domain.repository.CounterRepository

class HomeViewModel(private val repository: CounterRepository) : ViewModel() {
    private val _state = MutableStateFlow(HomeUiState())
    val state = _state.asStateFlow()
    
    
    
    fun increment() { viewModelScope.launch {
        try {
            val next = repository.load() + 1
            repository.save(next)
            _state.value = HomeUiState(next)
        } catch (error: CancellationException) { throw error } catch (error: Exception) { _state.value = _state.value.copy(error = "Unable to update counter")  }
    } }
    class Factory(private val repository: CounterRepository) : ViewModelProvider.Factory {
        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewModel> create(modelClass: Class<T>): T { require(modelClass.isAssignableFrom(HomeViewModel::class.java)); return HomeViewModel(repository) as T }
    }
}

What to notice

HomeViewModel keeps MutableStateFlow private and exposes a read-only StateFlow. It launches repository work in viewModelScope. CancellationException is rethrown so cancellation is not converted into an ordinary user-facing failure.

The factory receives a repository explicitly. Selecting Hilt adds integration scaffolding, but this ViewModel still uses the generated factory; it is not an @HiltViewModel. Keep this path or deliberately migrate the screen and ViewModel together.

State initially contains zero and the increment operation loads from the repository. A restored screen can therefore display zero until an action runs. Add a separate initial-load path when replacing memory with persistent storage, and preserve content during recoverable errors.

Extend it and verify the result

Implement a repository that fails its first save and succeeds on retry. Make the screen show the error without discarding the count, then clear that error after a successful retry. For coroutine tests, install a test Main dispatcher and control scheduling so assertions do not race the launched operation.

  • A successful action updates count and clears the old error.
  • A failed save preserves the previous count.
  • Cancelling the ViewModel does not become a failure message.
  • Recreating the screen restores the intended repository value.

These are exercises to add to your application tests, not claims that the starter already handles every case.

Try this configuration

MVVM · Jetpack Compose · Hilt · Retrofit · StateFlow

The generator includes a working counter example with a repository contract, selected dependencies, navigation, and local setup instructions. Extend the example around your own domain before shipping.

Configure your Android project →

For prerequisites, exact setup commands, expected behavior, and build troubleshooting, follow the android download-to-first-run walkthrough. Check the validation record for the configurations actually checked.

Continue reading

Primary reference

Android UI layer guidance. The source walkthrough above describes AppScaffold’s implementation; platform documentation describes the broader APIs and recommendations.