Jetpack ComposeMiddleSystem design

Как Jetpack Compose интегрируется с компонентом архитектуры ViewModel?

Jetpack Compose получает ViewModel через hiltViewModel() или viewModel() из compose-lifecycle-viewmodel, подписывается на StateFlow/LiveData через collectAsStateWithLifecycle(), а ViewModel живёт дольше composable — до уничтожения NavBackStackEntry.

Интеграция ViewModel с Jetpack Compose

ViewModel — компонент Android Architecture Components, хранящий UI-состояние пережив recomposition и повторное создание Activity/Fragment. Compose предоставляет специальные API для работы с ViewModel без потери lifecycle-безопасности.

Подключение зависимостей

// build.gradle.kts
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.0")
// Для Hilt:
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")

Получение ViewModel в Composable

import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.hilt.navigation.compose.hiltViewModel

// Без DI — для простых случаев
@Composable
fun ProfileScreen(
    viewModel: ProfileViewModel = viewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileContent(state = uiState, onEvent = viewModel::onEvent)
}

// С Hilt — рекомендованный подход для production
@Composable
fun ProfileScreen(
    viewModel: ProfileViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    ProfileContent(state = uiState, onEvent = viewModel::onEvent)
}

Подписка на StateFlow и LiveData

class ProfileViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(ProfileUiState())
    val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()

    // Также LiveData поддерживается
    val liveData: LiveData<String> = MutableLiveData("hello")

    fun onEvent(event: ProfileEvent) { /* ... */ }
}

@Composable
fun ProfileContent(viewModel: ProfileViewModel = hiltViewModel()) {
    // StateFlow — предпочтительный способ
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    // LiveData — для совместимости с legacy-кодом
    val ldValue by viewModel.liveData.observeAsState(initial = "")
}

ViewModel scope в Navigation Compose

По умолчанию hiltViewModel() создаёт ViewModel в scope текущего NavBackStackEntry. Для shared ViewModel между экранами используют родительский NavBackStackEntry:

@Composable
fun CheckoutScreen(
    navController: NavController,
    viewModel: CheckoutViewModel = hiltViewModel(
        navController.getBackStackEntry("checkout_graph") // scope на граф
    )
) { ... }

SavedStateHandle для аргументов навигации

class ItemViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    repo: ItemRepository
) : ViewModel() {

    // Получаем аргумент из Navigation Compose
    private val itemId: String = checkNotNull(savedStateHandle["itemId"])

    val item: StateFlow<Item?> = repo
        .getItem(itemId)
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
}

Подводные камни

  • Передача ViewModel напрямую вглубь дерева composable создаёт жёсткую зависимость и ломает preview.
  • viewModel() без явного указания scope может создать несколько экземпляров в Navigation Compose.
  • collectAsState() вместо collectAsStateWithLifecycle() не останавливает сбор в background — лишняя нагрузка на CPU и батарею.
  • Хранение Context или View в ViewModel вызывает утечку памяти — ViewModel переживает Activity.
  • Запуск корутин в ViewModel без viewModelScope теряет cancellation при очистке.
  • SharingStarted.Eagerly в stateIn держит Flow активным, даже когда UI не видит экран.
  • Изменяемый state напрямую из UI (минуя onEvent) нарушает Single Source of Truth.

Common mistakes

  • Объяснять «ViewModel и Compose» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Хранение NavController, Context или mutable UI state без границ во ViewModel усложняет lifecycle и тесты.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «ViewModel и Compose» своими словами и связывает ее с кодом.
  • Называет механизм: StateFlow обычно collectAsStateWithLifecycle на Android, а одноразовые эффекты требуют отдельной модели событий.
  • Видит production-последствие: Хранение NavController, Context или mutable UI state без границ во ViewModel усложняет lifecycle и тесты.

Sources

Related topics