MVVM Architecture Setup for Android App
MVVM — de facto standard for Android since Google released Architecture Components in 2017. ViewModel + LiveData/StateFlow + Repository — this combination described in official guidelines, supported by Jetpack and understood by any Android developer. Problem not that MVVM complex — but easy to implement wrong.
Typical mistakes breaking architecture
ViewModel with context. If ViewModel holds Context or Activity/Fragment reference — memory leak and pattern violation. ViewModel outlives Activity recreation on screen rotation. For resource access use AndroidViewModel with Application context only when no alternative, or move strings/resources to separate layer.
LiveData in Repository. Repository returns LiveData<List<User>> — now Repository tied to Android framework. Correct: Repository works with Flow<List<User>> (coroutines), ViewModel converts via stateIn or .asLiveData().
Business logic in ViewModel. ViewModel should transform data for UI, not implement business rules. Complex logic — in UseCase classes between ViewModel and Repository.
How correct MVVM looks on Kotlin
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val getUserProfile: GetUserProfileUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
fun loadProfile(userId: String) {
viewModelScope.launch {
getUserProfile(userId)
.onSuccess { _uiState.value = ProfileUiState.Success(it) }
.onFailure { _uiState.value = ProfileUiState.Error(it.message) }
}
}
}
sealed class ProfileUiState {
object Loading : ProfileUiState()
data class Success(val profile: UserProfile) : ProfileUiState()
data class Error(val message: String?) : ProfileUiState()
}
In Fragment or Composable subscribe to uiState via collectAsStateWithLifecycle() — safer than collect because automatically stops collection on background transition.
Repository and data sources
Repository — single entry point for ViewModel into data. Implements interface protocol. Inside decides: take from Room, from Retrofit or from cache:
class UserRepositoryImpl @Inject constructor(
private val api: UserApi,
private val dao: UserDao
) : UserRepository {
override fun getProfile(id: String): Flow<UserProfile> = flow {
val cached = dao.getUser(id)
if (cached != null) emit(cached.toDomain())
val remote = api.fetchUser(id)
dao.insert(remote.toEntity())
emit(remote.toDomain())
}
}
Hilt for DI
Without Hilt in Android MVVM must manually create ViewModelFactory. Hilt (@HiltViewModel + @Inject) eliminates this: Dagger graph generated at compile time, config errors visible immediately, not at runtime.
What we set up
Package structure: data (api, db, repository impl), domain (models, repository interfaces, use cases), presentation (viewmodels, ui). Hilt integration. Base Repository setup with Room + Retrofit. Example ViewModel with StateFlow and sealed class for UI states. Tests via kotlinx-coroutines-test and turbine.
Timelines
MVVM structure setup from scratch: 2–4 days. Refactoring existing Activity-based project: 1–3 weeks. Cost calculated individually.







