Оригиналы этих постов можно почитать в тг канале НеКрутой Архитектор
Там набирается материал для будущих статей с сильным опережением

План:

🧩 Собираем MVI-пазл воедино

Давай еще раз вспомним, в чем именно заключается идея MVI

Я правда пытался найти точное определение, но все источники, просто описывают, мол это и это является принципами MVI, вот как приготовить...

Вот самый ранний доклад об MVI, который я смог найти

Короче

Главная идея MVI заключается в единственном источнике правды для представления (State), который мы получаем по однонаправленному потоку событий

И уже из этой идеи вытекают следующие ограничения

Чтобы поток оставался однонаправленный, State обязан быть неизменяемым

Изменение неизменяемого состояния возможно только через создание нового State

Чтобы это вписывалось в однонаправленный поток данных, создание должно происходить через чистую функцию Reducer

Чтобы Reducer понимал, что нужно сделать с текущим состоянием, ему нужен "идентификатор" изменения, которым является Intent

Такое простое определение обязывает нас создать State, Reducer и Intent

State при этом нужно где-то хранить = Store

Кто-то должен работать = Actor

И хорошо бы следить за тем, что происходит = Interceptor

Вроде простая идея (единое неизменяемое состояние через однонаправленный поток данных), а порождает сразу 6 сущностей, без которых желаемого не добиться

Главная сложность в понимании MVI, как раз и кроется в его простой идее

MVI не регламентирует, КАК вы будете его реализовывать

MVP и MVVM как бы тоже это не регламентируют, но у них есть ОДНА главная сущность, название которой заложено в названии самого шаблона

MVI же, порождает целых 6 сущностей, но название дает только одной Intent, и то, в Android его многие хотят изменить, чтобы не ассоциироваться с классом Intent от самого SDK

Мы еще обсудим названия, а пока давайте ответим на такой вопрос

Можно ли иметь MVP и MVI на одном экране?  

Прокачанный Presenter (например через Moxy) умеет переживать состояние

  • это позволяет ему хранить текущее состояние, то есть выступать в роли Store

Presenter знает о жизненном цикле View

  • это позволяет ему отменять операции в случае ухода с экрана,
    то есть выступать в роли Actor

Каждый раз, при изменении состояния (State), Presenter может передавать его во View через метод render(State)

Все остальные сущности MVI являются
либо объектами данных (Intent, State),
либо чистыми функциями (Reducer и Interceptor)

Так что правильный ответ ДА!
MVP и MVI можно сварить вместе!

Ровно тоже самое будет и с MVVM, и даже еще лучше, потому что View будет подписана на State, продолжая тем самым цепочку потока данных

В этом месте можно уже смело ответить на вопрос:

Можно ли ViewModel, которая просто получает все события в один метод и возвращает единый State, называть MVI?

НЕТ, нельзя.
Если вы не реализовали еще и единый поток данных через Reducer и Actor,
то вы все еще остаетесь в шаблоне MVVM with single State

А что еще?

Есть еще замечательная библиотека Decompose, в которой есть сущность Component

У него свой жизненный цикл, что позволяет ему хранить состояние (Store)
и к которому можно привязать отмену запущенной работы (Actor)

В конце концов, даже на чистом Activity можно приготовить MVI!

А как выглядит самая простая реализация MVI в Kotlin?
// Описываем состояние
@Parcelize
data class State(
  val items: List<String>? = null
) : Parcelable

// Перечисляем все возможные события
interface Intent {
  // Reducer тут, чтобы избежать огромного when
  fun reduce(state: State): State

  data class RestoreState(val restoredState: State) : Intent {
    override fun reduce(state: State): State {
      return restoredState
    }
  }

  data class Loaded(val items: List<String>) : Intent {
    override fun reduce(state: State): State {
      return state.copy(items = items)
    }
  }
  // и т.д.
}

// Cоздание Actor, но не вызов
// MutableSharedFlow для проброса новых Intent в начало цепочки
fun MutableSharedFlow<Intent>.createActor(
  featureCase: FeatureCase, // Передача зависимостей
): suspend (State) -> Unit { // Функция возвращает функцию
  return { state ->
    // Запуск задачи по состоянию (2 вариант из 3 статьи)
    // !!! Но тут нужно выбрать решение, 
    // !!! чтобы другие изменения стейта не запускали повторную работу
    if (state.items == null) {
      featureCase.invoke() // вызов Model
        .onSuccess { items -> emit(Intent.Loaded(items)) }
        .onFail { error -> emit(Intent.Failed(error.message)) }
    }
  }
}

// Сахар для вызова actor
fun <S> Flow<S>.action(actor: suspend (S) -> Unit): Flow<S> {
  return onEach { state ->
    supervisorScope { // Ошибки не должны рушить цепочку
        actor(state)
    }
  }
}

// Создание Store (да вот такой простой)
// scan хранит состояние
// отмена работы через отмену корутины
fun Flow<Intent>.toState(
  actor: suspend (State) -> Unit,
): Flow<State> {
  return scan(State()) { state, intent -> intent.reduce(state) } 
    .action(actor)
}

// До этого момента мы только описали "рецепт" MVI, 
// но так и не запустили его


// Добавляем MVI в чистый Activity
class MyActivity : AppCompatActivity() {
  // Тут DI
  private val featureCase: FeatureCase = ... 

  // Сюда события кинешь
  private val intentFlow = MutableSharedFlow<Intent>(
    replay = 1,
    onBufferOverflow = BufferOverflow.DROP_OLDEST
  )

  // Тут MVI создашь
  private val stateFlow: StateFlow<State> = intentFlow
    .toState(actor = intentFlow.createActor(featureCase))
    // Тут MVI запустишь
    .stateIn(coroutineScope(), Eagerly, State())

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    // ...

    // Тут состояние восстановишь
    savedInstanceState?.getParcelable<State>("screen_state")
      ?.let { savedState ->
        intentFlow.tryEmit(Intent.RestoreState(savedState))
      }

    // Тут свежее состояние достанешь
    lifecycleScope.launch {
      stateFlow.collect { state -> render(state) }
    }

    setupUserInteractions()
  }

  // Тут состояние сохранишь
  override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putParcelable("screen_state", stateFlow.value)
  }

  // Тут события привяжешь
  private fun setupUserInteractions() { /* ... */ }

  // Тут UI нарисуешь
  private fun render(state: State) { /* ... */ }
}

🤔 А что если вообще написать свою реализацию MVI?

Вот тут то и начинается самая сложность

Дело в том, что все те названия, которые я вам называл,
не регламентируются шаблоном MVI

Что это значит?

Каждый, кто реализует библиотеку для MVI, волен называть сущности по своему
Даже Intent в названии их не останавливает

Intent или Action, Event, Wish, Call

State - ну хоть тут вроде все единодушны

Store или Feature, Presenter, ViewModel, Component, Container

Actor или AsyncWorker, Executor, Transformer, Model?? или просто часть Store

Reducer - тут вроде тоже все сошлись на одном

Interceptor или Middleware

Если Actor стоит первым, то он может отдавать в Reducer: Mutation или Transform

Если Actor после Reducer, то можно встретить SideEffect, Task, AsyncEvent

При этом SideEffect (или News, SingleEvent) может еще обозначать одиночные события для View, типа "покажи тост"
Так не нужно делать, для идеи MVI это костыль
Вот тут можно почитать, почему такие события нарушают идею MVI

Так же могут появляться дополнительные сущности,
для решения различных задач или особенностей реализации:
Bootstrapper, Binder, PostProcessor

Короче зоопарк еще тот, из-за которого и возникает основная сложность в освоении MVI

Но если ты поймешь суть каждой из сущностей,
то сможешь легко определять кто есть кто во всех этих библиотеках

📜 Ты так и не понял, что такое Model?

Вроде уже сотни раз, все кому не лень, рассказали что такое эта ваша Model
🫤 Но у многих все равно остается ощущение неуверенности в своем понимании

А правильно ли я понимаю Model?
А правильно ли понял Model тот, кто писал статью?
А кто-то вообще понимает правильно, что такое Model?
А вот в другой статье говорили по другому...

Кто-то считает, что Model это только данные
Другие говорят, что Model это слой доступа к данным
Третьи утверждают, что Model это бизнес-логика, что бы это не значило

😎 Думаете я скажу, что они все не правы, а вот вам истина в последней инстанции?

И да и нет

🤓 Правы ли другие?

И да и нет

Каждый из них отчасти является правым

И проблема здесь заключается в том, что каждый говорит про Model со своего ракурса,
учитывая специфику своей платформы, своего проекта,
своего времени и последних трендов в технологиях,
или же просто общими терминами из определения самого шаблона

Давайте еще раз вспомним, для чего вообще существуют MV шаблоны

MV шаблоны - это про взаимодействие клиента и функциональности
View - это то, как функциональность воспринимает клиент
Некая *** - это связь между функциональностью и тем, что воспринимает клиент
Соответственно, Model - это сама функциональность, ради которой клиент и пришел

🍔 Давайте на примере ресторана

Данные - это еда
View - это интерьер, сервированный стол и тарелка с едой
* - это официант, который передает заказ на кухню и приносит еду клиенту
Model - это кухня, на которой готовится еда

🍽  Является ли кухня едой?

Нет, но она составляет меню

🍅  Выращивают ли повара помидоры для салата?

Нет, но они из них готовят

Можно еще долго говорить, чего кухня не делает или делает,
но в рамках шаблона, это ВООБЩЕ не имеет значения 🕳

Шаблон описывает только процесс работы официанта 🏃‍♂️‍➡️
Именно по этому все статьи концентрируются только на этих звездочках ***

В рамках шаблона, Model это вообще всё то, что происходит для приготовления еды,
и повара, и поставщики, и склады, и ферма

Из этого понимания, можно сделать такое определение модели,
которое на мой взгляд будет наиболее точно отражать ее суть:

Model - это совокупность всех сущностей, которые выполняют нужную клиенту работу и предоставляют результат в виде данных

Какие сущности, как они устроены, за что они отвечают, нам вообще не важно

Всё это проектируется отдельно и независимо от того,
как данные будут передаваться клиенту

Мы можем обратиться к их интерфейсу и получить результат их работы в виде данных

Надеюсь теперь, у тебя больше не осталось сомнений в понимании того,
что такое
Model и с чем ее едят 🐛