Skip to content
ArceApps Logo ArceApps
ES

MVVM: The View Layer Guide

⏱️ 2 min read
MVVM: The View Layer Guide

🖼️ The Role of the View

In MVVM, the View (Activity, Fragment, or Composable) is responsible only for rendering the UI and capturing user interactions. It should contain zero business logic.

Responsibilities

  1. Render State: Display data from the ViewModel.
  2. Capture Events: Click listeners, text input.
  3. Navigate: Handle navigation actions (though logic often resides in ViewModel).

⚡ The Pattern: Unidirectional Data Flow (UDF)

  1. State flows down: ViewModel -> View.
  2. Events flow up: View -> ViewModel.

Example: Collecting State in Compose

@Composable
fun UserScreen(
    viewModel: UserViewModel = hiltViewModel()
) {
    // Collect state safely with lifecycle awareness
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when (val state = uiState) {
        is UserUiState.Loading -> LoadingScreen()
        is UserUiState.Success -> UserList(state.users)
        is UserUiState.Error -> ErrorScreen(state.message)
    }
}

⚠️ Common Anti-Patterns

1. Logic in UI

Wrong: Calculating list filters inside lazyColumn. Right: Filter in ViewModel, expose the filtered list.

2. State Hoisting Failures

Wrong: Passing UserViewModel deep down the widget tree. Right: Pass only data (List<User>) and lambdas (onUserClick: (String) -> Unit) to child composables. This makes them reusable and testable.

// Reusable Component - Knows nothing about ViewModel
@Composable
fun UserList(
    users: List<User>,
    onUserClick: (String) -> Unit
) { ... }

3. Ignoring Lifecycle

Wrong: Launching coroutines in LaunchedEffect(Unit) without considering screen rotation or backgrounding. Right: Rely on ViewModel’s scope or lifecycleScope.

🔄 Handling One-Time Events (Navigation, Toasts)

This is tricky in Compose. The recommended approach is to model events as part of the state or use a separate SharedFlow / Channel.

Using Effect

val event = viewModel.eventFlow.collectAsStateWithLifecycle(initialValue = null)

LaunchedEffect(event.value) {
    event.value?.let { e ->
        when(e) {
            is UiEvent.ShowToast -> context.toast(e.message)
            is UiEvent.Navigate -> navController.navigate(e.route)
        }
        viewModel.onEventConsumed() // Clear event to avoid re-trigger on rotation
    }
}

🏁 Conclusion

A clean View layer is dumb. It blindly reflects the state provided by the ViewModel. This makes UI tests trivial and ensures consistency.

You might also be interested in

MVVM Model: The Invisible but Vital Data Layer
Android October 2, 2025

MVVM Model: The Invisible but Vital Data Layer

The 'Model' in MVVM is much more than data classes. Learn to design a robust model layer that survives UI and backend changes.

Read more
MVVM Architecture in Android: The Comprehensive Guide (2025)
Android October 1, 2025

MVVM Architecture in Android: The Comprehensive Guide (2025)

Master the Model-View-ViewModel pattern from basic concepts to advanced implementations with practical examples of a Minesweeper game for Android.

Read more
SOLID Principles: Android Examples
SOLID June 21, 2025

SOLID Principles: Android Examples

Understanding SOLID principles in modern Android. Examples using Kotlin, Hilt, and MVVM.

Read more