Код-ревью Senior ViewModel и сохранение состояния обновлено

Код-ревью ViewModel профиля

Вопрос

Проведи ревью ViewModel экрана профиля. Найди ошибки и места, которые стоит улучшить.

class ProfileViewModel(
    private val activity: ProfileActivity,
    private val repo: ProfileRepository,
) : ViewModel() {
    val state = MutableStateFlow<ProfileUiState>(ProfileUiState.Loading)
    var draftName: String = ""

    fun load(userId: String) {
        GlobalScope.launch(Dispatchers.Main) {
            try {
                val profile = repo.loadProfile(userId)
                state.value = ProfileUiState.Content(profile)
            } catch (e: Exception) {
                state.value = ProfileUiState.Error(e.message ?: "")
                activity.showToast("Ошибка")
            }
        }
    }

    fun onNameChanged(name: String) {
        draftName = name
        state.value = (state.value as ProfileUiState.Content).copy(name = name)
    }
}

Коротко

Главные проблемы: утечка Activity через конструктор и GlobalScope вместо viewModelScope. Показ тоста должен быть событием или частью состояния, а не вызовом Activity.

Что должен найти ревьюер

  1. Ошибкастрока 2
    Ссылка на Activity во ViewModel: после поворота ViewModel держит уничтоженную Activity, утечка памяти и тост на мёртвом экране
  2. Стильстрока 5
    Публичный MutableStateFlow: UI может менять состояние в обход ViewModel; нужен приватный _state и публичный StateFlow
  3. Ошибкастрока 6
    Черновик имени в обычном поле теряется при смерти процесса; хранить в SavedStateHandle
  4. Ошибкастрока 9
    GlobalScope: запрос не отменится при закрытии экрана; нужен viewModelScope
  5. Ошибкастрока 13
    catch (e: Exception) глотает CancellationException, отменённая корутина продолжит работу
  6. Ошибкастрока 22
    Небезопасное приведение as Content: если состояние Loading или Error, ClassCastException; использовать update и проверку типа

Разбор

Главные проблемы: утечка Activity через конструктор и GlobalScope вместо viewModelScope. Показ тоста должен быть событием или частью состояния, а не вызовом Activity. Состояние наружу отдаётся только для чтения. Ввод пользователя, который должен пережить смерть процесса, хранится в SavedStateHandle. Обработка ошибок не должна перехватывать отмену, а изменение состояния лучше делать через _state.update { } с проверкой текущего варианта.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.

Термины по теме