Урок по треку Android 7 мин

ViewModel и сохранение состояния за 7 минут

Как ViewModel переживает поворот, viewModelScope, SavedStateHandle и process death.

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?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.