Что такое стейт-машина в MVI

Ответ

Стейт-машина в MVI (Model-View-Intent) — это концепция, где состояние экрана (State) изменяется предсказуемо в ответ на события (Intents) через чистые функции (редьюсеры). Каждое новое состояние — это неизменяемый объект, который View отображает.

Пример:

data class LoginState(
    val isLoading: Boolean = false,
    val error: String? = null,
    val isSuccess: Boolean = false
)

fun reduce(oldState: LoginState, intent: LoginIntent): LoginState {
    return when (intent) {
        is LoginIntent.Submit -> oldState.copy(isLoading = true)
        is LoginIntent.Error -> oldState.copy(isLoading = false, error = intent.message)
        is LoginIntent.Success -> oldState.copy(isLoading = false, isSuccess = true)
    }
}

Ключевые моменты:

  • Состояние иммутабельно.
  • Изменения только через редьюсеры.
  • Предсказуемость: одинаковый интент + состояние = одинаковый результат.

Ответ 18+ 🔞

Э, слушай, давай разберём эту вашу стейт-машину в MVI, а то народ пугается, как будто это квантовая физика какая-то. На самом деле всё проще, чем кажется, если не забивать голову умными словами.

Представь себе, что у тебя есть экран — ну, например, логин. И у этого экрана есть состояние, оно же State. Это как настроение у человека: либо он спокойный, либо грузит мозги, либо обделался с ошибкой, либо, наоборот, такой довольный, что всё получилось. Всё это — разные состояния одного и того же чувака (экрана).

Вот мы это состояние описываем обычным дата-классом, чтоб всё по полочкам:

data class LoginState(
    val isLoading: Boolean = false, // грузится ли он, как долбоёб
    val error: String? = null,      // если обосрался — тут будет сообщение об ошибке
    val isSuccess: Boolean = false  // если всё заебок — флаг в true
)

Теперь, интенты (Intents) — это как раз все события, которые могут с этим экраном произойти. Пользователь тыкнул кнопку «Отправить» — это интент. Сервер вернул ошибку — это интент. Всё прошло успешно — опять интент. Это просто сигналы: «эй, чувак, что-то случилось!».

А теперь самое важное, где вся магия и предсказуемость. У нас есть редьюсер — это такая чистая функция, которая берёт старое состояние и интент, который прилетел, и возвращает новое состояние. Без всяких побочных эффектов, без рандома, чистая математика, ёпта. Как в том анекдоте: «Дважды два — четыре, и хуй с горы».

fun reduce(oldState: LoginState, intent: LoginIntent): LoginState {
    return when (intent) {
        is LoginIntent.Submit -> oldState.copy(isLoading = true) // только начали грузиться
        is LoginIntent.Error -> oldState.copy(isLoading = false, error = intent.message) // обосрались
        is LoginIntent.Success -> oldState.copy(isLoading = false, isSuccess = true) // победили
    }
}

Видишь? oldState.copy(...). Мы не меняем старое состояние, мы создаём новый объект на его основе. Это и есть иммутабельность. Старое состояние остаётся как было, его хоть в музей отправляй. А View просто берёт и отрисовывает то, что в новом состоянии прописано.

Итоговые ключевые моменты, чтобы в голове отложилось:

  • Состояние иммутабельно. Это священная корова. Не трогай старое, создавай новое. Иначе потом сам от себя охуеешь, ища баг.
  • Изменения только через редьюсеры. Это единственные ворота. Никаких прямых присваиваний полей из View или презентера. Всё через интенты и редьюсер. Порядок, блядь.
  • Предсказуемость. Это главный плюс, ради которого всё и затевалось. Одинаковые входные данные (старое состояние + интент) всегда дают одинаковый результат. Никаких «ой, а тут у меня в прошлый раз сработало». Не, тут так не прокатит. Как дважды два.

Вот и вся философия. Не так страшен чёрт, как его малюют. Сначала кажется, что овердохуища кода для простой кнопки, но когда проект растёт и состояний становится много, эта предсказуемость начинает спасать жопу.