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 и тесты.