ViewModel это место для состояния экрана и логики представления. На собесе проверяют, понимаете ли вы, почему она переживает поворот, чего ей нельзя хранить и как пережить смерть процесса.
Зачем ViewModel
Activity и фрагменты пересоздаются при смене конфигурации. Если держать данные экрана в их полях, после поворота всё загрузится заново. ViewModel живёт дольше: от первого создания экрана до его окончательного закрытия.
class ProfileViewModel(
private val repo: ProfileRepository,
private val savedState: SavedStateHandle,
) : ViewModel() {
private val _state = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)
val state: StateFlow<ProfileUiState> = _state.asStateFlow()
init { load() }
fun load() {
viewModelScope.launch {
_state.value = try {
ProfileUiState.Content(repo.profile())
} catch (e: IOException) {
ProfileUiState.Error
}
}
}
}
Почему переживает поворот
ViewModel хранится в ViewModelStore, который принадлежит ViewModelStoreOwner (Activity, Fragment, NavBackStackEntry). При смене конфигурации Activity передаёт свой ViewModelStore новому экземпляру (через механизм non-configuration instance), и by viewModels() возвращает тот же объект. Когда экран закрывают окончательно (finish, «Назад»), store очищается и у ViewModel вызывается onCleared(), а viewModelScope отменяется.
Получение:
// Activity / Fragment
private val vm: ProfileViewModel by viewModels()
private val shared: CartViewModel by activityViewModels() // общая для фрагментов Activity
// Compose
val vm: ProfileViewModel = viewModel() // или hiltViewModel()
Если у ViewModel есть параметры конструктора, нужна фабрика: ViewModelProvider.Factory, viewModelFactory { initializer { } } или DI (Hilt создаёт фабрику сам через @HiltViewModel и @Inject constructor).
Чего нельзя хранить во ViewModel
ViewModel живёт дольше Activity, поэтому ссылка на Activity, Fragment, View или их Context это утечка: после поворота ViewModel держит уничтоженную Activity. Если нужен контекст для ресурсов, берут Application (AndroidViewModel или внедрение @ApplicationContext), но лучше вынести работу с ресурсами в отдельный слой.
Также не передают во ViewModel лямбды, захватывающие Activity, и не подписываются из ViewModel на LiveData через observeForever без отписки.
viewModelScope
viewModelScope это CoroutineScope с SupervisorJob и Dispatchers.Main.immediate, отменяется в onCleared. Запросы, запущенные в нём, переживают поворот (ViewModel та же) и отменяются при закрытии экрана. Не путать с lifecycleScope Activity, который умирает при каждом повороте.
Смерть процесса и SavedStateHandle
ViewModel не переживает смерть процесса. SavedStateHandle это словарь, который сохраняется в Bundle и восстанавливается при пересоздании процесса:
class SearchViewModel(private val handle: SavedStateHandle) : ViewModel() {
val query: StateFlow<String> = handle.getStateFlow("query", "")
fun onQueryChange(q: String) { handle["query"] = q }
}
Туда кладут то, что пользователь ввёл или выбрал и что нельзя восстановить из базы: текст поиска, выбранный фильтр, id открытого элемента. Аргументы навигации тоже доступны через SavedStateHandle. Ограничения те же, что у Bundle: только небольшие сериализуемые данные.
Состояние наружу
Хорошая практика: наружу торчит только неизменяемый поток (StateFlow<UiState>), а изменяемый MutableStateFlow приватный. UI шлёт события вызовами методов (onQueryChange, onRetry), ViewModel меняет состояние. Это однонаправленный поток данных (UDF).
Не стоит запускать загрузку в init, если её параметры приходят позже, и не стоит грузить заново на каждую подписку UI. Для потоков из репозитория удобно stateIn(viewModelScope, WhileSubscribed(5_000), initial).
Тестирование
ViewModel тестируют без Android: подменяют Dispatchers.Main через Dispatchers.setMain(StandardTestDispatcher()) (или UnconfinedTestDispatcher), репозитории фейками, и проверяют значения state в runTest.
Частые ошибки
- Ссылка на Activity, View или их Context во ViewModel.
- Ожидать, что ViewModel переживёт смерть процесса.
- Большие данные в
SavedStateHandle. - Публичный
MutableStateFlow, который меняют из UI. - Создание ViewModel через конструктор (
ProfileViewModel()) вместо провайдера: каждый поворот новый объект. - Загрузка данных в
onResumeэкрана вместо одного раза во ViewModel.
Что спросят на собесе
- Почему ViewModel переживает поворот? Где она хранится?
- Когда вызывается
onCleared? - Почему нельзя хранить Context Activity во ViewModel?
- Что такое
SavedStateHandleи чем он отличается от ViewModel? - Как расшарить ViewModel между фрагментами?
- Чем
viewModelScopeотличается отlifecycleScope?