Modern Android development demands scalable, maintainable, and testable architectures, and MVVM (Model-View-ViewModel) has emerged as the gold standard. It helps in structuring code in a way that ensures a clean separation of concerns, making it easier to manage UI, business logic, and data operations.
In this guide, weโll take an in-depth look at MVVM, its benefits, how to implement it using Jetpack Compose, and advanced concepts like dependency injection, UI state handling, and testing. Letโs dive in!
What isย MVVM?
MVVM (Model-View-ViewModel) is an architectural pattern that separates the presentation layer (UI) from the business logic and data handling. This separation enhances modularity, making the app easier to maintain and test.
MVVM Components
- Model: Represents the data layer (API, database, repositories) and business logic.
- View: The UI layer (Activity, Fragment, or Composable functions in Jetpack Compose).
- ViewModel: Acts as a bridge between View and Model, holding UI-related data and surviving configuration changes.
How MVVMย Works
- The View observes data from the ViewModel.
- The ViewModel fetches data from the Model.
- The Model retrieves data from an API, database, or local cache.
- The ViewModel exposes the data, and the View updates accordingly.
Why Useย MVVM?
- Separation of ConcernsโโโKeeps UI and business logic separate.
- Better TestabilityโโโViewModel can be unit tested without UI dependencies.
- Lifecycle AwarenessโโโViewModel survives configuration changes.
- ScalabilityโโโWorks well with large-scale applications.
- Compatibility with Jetpack ComposeโโโSupports modern UI development in Android.
Implementing MVVM in Android with Jetpackย Compose
Letโs implement a simple MVVM architecture using Jetpack Compose.
Step 1: Model (Repository Layer)
class UserRepository {
fun getUsers(): List<String> {
return listOf("Amol", "Akshay", "Swapnil")
}
}
Step 2: ViewModel
class UserViewModel : ViewModel() {
private val repository = UserRepository()
private val _users = MutableStateFlow<List<String>>(emptyList())
val users: StateFlow<List<String>> = _users
init {
fetchUsers()
}
private fun fetchUsers() {
_users.value = repository.getUsers()
}
}
Step 3: View (Jetpack Composeย UI)
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val users by viewModel.users.collectAsState()
LazyColumn {
items(users) { user ->
Text(text = user, fontSize = 20.sp, modifier = Modifier.padding(16.dp))
}
}
}
This basic example sets up MVVM with a repository, ViewModel, and a UI that observes data changes.
Dependency Injection in MVVM (Usingย Hilt)
To make the architecture scalable, we use Hilt for dependency injection.
Step 1: Add Dependencies
dependencies {
implementation "androidx.hilt:hilt-navigation-compose:1.1.0"
implementation "com.google.dagger:hilt-android:2.44"
kapt "com.google.dagger:hilt-compiler:2.44"
}
Step 2: Enable Hilt in the Application Class
@HiltAndroidApp
class MyApp : Application()
Step 3: Inject Repository into ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(private val repository: UserRepository) : ViewModel() {
private val _users = MutableStateFlow<List<String>>(emptyList())
val users: StateFlow<List<String>> = _users
init {
fetchUsers()
}
private fun fetchUsers() {
_users.value = repository.getUsers()
}
}
Step 4: Inject ViewModel into Composable
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
val users by viewModel.users.collectAsState()
LazyColumn {
items(users) { user ->
Text(text = user, fontSize = 20.sp, modifier = Modifier.padding(16.dp))
}
}
}
LiveData vs StateFlow: Which One toย Use?

Best Practice: Use StateFlow
with Jetpack Compose because it integrates better with collectAsState()
.
Handling UI State inย MVVM
To manage loading, success, and error states:
sealed class UIState<out T> {
object Loading : UIState<Nothing>()
data class Success<T>(val data: T) : UIState<T>()
data class Error(val message: String) : UIState<Nothing>()
}
Modify ViewModel:
class UserViewModel : ViewModel() {
private val _users = MutableStateFlow<UIState<List<String>>>(UIState.Loading)
val users: StateFlow<UIState<List<String>>> = _users
fun fetchUsers() {
viewModelScope.launch {
try {
val data = repository.getUsers()
_users.value = UIState.Success(data)
} catch (e: Exception) {
_users.value = UIState.Error("Failed to load users")
}
}
}
}
In UI:
when (state) {
is UIState.Loading -> CircularProgressIndicator()
is UIState.Success -> LazyColumn { items(state.data) { user -> Text(user) } }
is UIState.Error -> Text(state.message, color = Color.Red)
}
Unit Testing MVVM Components
Unit testing is important in MVVM to ensure reliability.
Test ViewModel
Add testing dependencies:
testImplementation "junit:junit:4.13.2"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
testImplementation "io.mockk:mockk:1.13.3"
Create a UserViewModelTest
file:
@ExperimentalCoroutinesApi
class UserViewModelTest {
private lateinit var viewModel: UserViewModel
private val repository = mockk<UserRepository>()
@Before
fun setUp() {
every { repository.getUsers() } returns listOf("Amol", "Akshay", "Swapnil")
viewModel = UserViewModel(repository)
}
@Test
fun `fetchUsers updates users state correctly`() {
// We can also call the body of setUp() here, which is useful for individual test functions that need more customization.
assert(viewModel.users.value is UIState.Success)
}
}
This tests that fetchUsers()
properly updates the UI state.
Conclusion
MVVM architecture enhances modularity, testability, and scalability in Android development. By using Jetpack Compose, Hilt for DI, StateFlow for state management, and UI state handling, we can build robust and maintainable applications.