Mastering MVVM Architecture in Android: A Complete Guide

Table of Contents

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

  1. Model: Represents the data layer (API, database, repositories) and business logic.
  2. View: The UI layer (Activity, Fragment, or Composable functions in Jetpack Compose).
  3. ViewModel: Acts as a bridge between View and Model, holding UI-related data and surviving configuration changes.

How MVVM Works

  1. The View observes data from the ViewModel.
  2. The ViewModel fetches data from the Model.
  3. The Model retrieves data from an API, database, or local cache.
  4. 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)

Kotlin
class UserRepository {
    fun getUsers(): List<String> {
        return listOf("Amol", "Akshay", "Swapnil")
    }
}

Step 2: ViewModel

Kotlin
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)

Kotlin
@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

Kotlin
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

Kotlin
@HiltAndroidApp
class MyApp : Application()

Step 3: Inject Repository into ViewModel

Kotlin
@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

Kotlin
@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:

Kotlin
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:

Kotlin
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:

Kotlin
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:

Kotlin
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:

Kotlin
@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.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!