Как приготовить MVI в 2024 часть 1

6d7bf7915a0dd47bd2bc2f46dc43e0c5

Привет, Хабр! Меня зовут Артем и я автор и ведущий YouTube канала Android Insights

Введение

Так получилось, что я большой фанат однонаправленных архитектур, а именно MVI, и вот почему:

  • Предсказуемость и контроль над состоянием

    MVI делает состояние приложения полностью управляемым. Каждое изменение состояния происходит через определённые действия (Intent), и состояние обновляется через reducer. Это упрощает отслеживание и отладку, так как в любой момент времени состояние приложения известно и поддается реконструкции.

  • Одна точка входа для управления состоянием

    В MVI состоянием управляет только один поток данных (Unidirectional Data Flow). Вся логика работы с состоянием приложения сосредоточена в одном месте, что упрощает понимание и поддержку кода.

  • Упрощенная отладка и тестирование

    Благодаря единственному источнику правды и строгим правилам изменения состояния, MVI упрощает написание unit-тестов. Легче воспроизводить сценарии для тестирования, так как каждое действие приводит к известным изменениям.

Но как и все в этом мире, MVI не лишен недостатков

  • Повышенная сложность структуры

    MVI — менее распространенный архитектурный паттерн по сравнению с MVP или MVVM, и разработчикам может потребоваться больше времени на его освоение. Особенно сложно может быть тем, кто не знаком с функциональным подходом к программированию или реактивными потоками.

  • Избыточность для простых приложений

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

Лично для меня, имеющиеся недостатки перекрываются плюсами и возможностями MVI подхода. Я довольно давно открыл для себя данную архитектуру, с тех пор активно ее практикую и применяю.

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

Так или иначе я пришел к тому, что решил создать свое собственное решение.

Итак, представляю вашему вниманию SimpleMVI

Но прежде чем начать рассматривать библиотеку, давайте вспомним (или изучим), что такое этот ваш MVI вообще?

Что такое MVI?

MVI (Model-View-Intent) — это архитектурный паттерн, используемый в разработке мобильных приложений, который фокусируется на управлении состоянием приложения через потоки данных. В MVI всё взаимодействие можно разбить на три основных компонента:

  • Model — это часть, которая отвечает за хранение состояния приложения и логику его изменения. Состояние в MVI считается единственным источником правды, что упрощает контроль и предсказуемость приложения.

  • View — это интерфейс пользователя. Она отображает текущее состояние (Model) и реагирует на изменения. View не содержит логики, а только показывает данные, полученные из Model.

  • Intent — это действия пользователя или события, которые генерируют новые изменения в состоянии. Intents передаются в Model для обработки и изменения состояния.

MVI возник как эволюция архитектурных паттернов для управления состоянием в мобильных и веб-приложениях. Корни данного подхода можно проследить до архитектуры Cycle.js и принципов, которые были использованы в паттерне Redux для JavaScript.

Основная идея MVI — это однонаправленный поток данных и управление состоянием через непрерывные потоки. Паттерн пришел в мобильную разработку как более современная и чистая альтернатива традиционным паттернам вроде MVC и MVP, которые часто приводили к сложности при масштабировании и тестировании. Вдохновением для MVI послужил успех библиотеки Redux, которая показала эффективность однонаправленного потока данных для управления состоянием интерфейсов.

Таким образом, MVI — это результат поисков более предсказуемого и управляемого подхода к архитектуре приложений, особенно в условиях сложных пользовательских интерфейсов с большим количеством состояний и интерактивных элементов.

Теперь, когда мы разобрались с тем, что такое MVI и откуда он пришел, самое время вернуться к его реализации, а именно к SimpleMVI!

Что такое SimpleMVI?

SimpleMVI — современная, гибкая и мощная библиотека, которая помогает реализовывать MVI архитектуру в ваших проектах. Проектируя библиотеку, я старался сделать ее максимально простой в использовании, что отражено в названии. В SimpleMVI всего два основных интерфейса, первый из них — Store

Интерфейс Store

Для начала давайте покажу, как выглядит этот интерфейс

Скрытый текст

public interface Store {

    public val state: State

    public val states: StateFlow
  
    public val sideEffects: Flow

    @MainThread
    public fun init()

    @MainThread
    public fun accept(intent: Intent)

    @MainThread
    public fun destroy()
}

Каждый Store должен быть параметризован тремя параметрами:

  • Intent — определяет действия, которые Store умеет обрабатывать

  • State — описывает состояние Store

  • SideEffect — события, которые могут происходить внутри Store во время обработки Intent, но не приводят к изменению State

Текущее состояние Store можно получить, вызвав свойство state, также можно подписаться на поток состояний Store, используя свойство states.

Также Store может производить SideEffect. SideEffect — это событие, которое может произойти и вы хотите сообщить о нем внешнему миру, но в то же время это событие не приводит к изменению состояния Store.

Каждый Store должен быть проинициализирован, через вызов функции init (), и уничтожен посредством вызова функции destroy ()

Создать Store можно посредством вызова функции createStore

Скрытый текст

public fun  createStore(
    initialize: Boolean = true,
    coroutineContext: CoroutineContext = Dispatchers.Main.immediate,
    initialState: State,
    initialIntents: List = emptyList(),
    middlewares: List> = emptyList(),
    actor: Actor,
): Store {
    return DefaultStore(
        coroutineContext = coroutineContext,
        initialState = initialState,
        initialIntents = initialIntents,
        middlewares = middlewares,
        actor = actor,
    ).apply {
        if (initialize) {
            init()
        }
    }
}

Рассмотрим, за что отвечает каждый параметр данной функции

  • initialize — отвечает за то, будет ли Store инициализирован сразу в момент создания

  • coroutineContext — контекст корутин, в котором будет работать Store

  • initialState — начальное состояние Store

  • initialIntents — список Intent, которые будут обработаны после инициализации

  • middlewares — список Middleware для данного Store, будут рассмотрены в дальнейших статьях

  • actor — второй базовый интерфейс в SimpleMVI. Данный объект реализует логику, остановимся на нем подробнее

Интерфейс Actor

Скрытый текст

public interface Actor {

    @MainThread
    public fun init(
        scope: CoroutineScope,
        getState: () -> State,
        reduce: (State.() -> State) -> Unit,
        onNewIntent: (Intent) -> Unit,
        postSideEffect: (sideEffect: SideEffect) -> Unit,
    )

    @MainThread
    public fun onIntent(intent: Intent)

    @MainThread
    public fun destroy()
}

Actor отвечает за реализацию логики конкретного Store. Он занимается обработкой Intent, может создавать новый State и отправлять SideEffect.

Сам интерфейс содержит всего три функции:

  • init — привязывает Actor к Store инициализирует его

  • onIntent — данная функция вызывается, когда Store получает новый Intent

  • destroy — вызывается в момент уничтожения Store, в ней можно очистить ресурсы

Пример создания Store

Создать Store довольно просто, рекомендуемый способ — Объявить класс, например, MyCoolStore, создать классы, которые описывают Intent, State и SideEffect для MyCoolStore.

Также MyCoolStore должен реализовывать интерфейс Store.

Для реализации интерфейса Store можно воспользоваться делегированием, о котором я рассказывал в своей прошлой статье.

Вот как будет выглядеть финальный результат:

Скрытый текст

class MyCoolStore : Store by createStore(
    initialState = State(),
    actor = actorDsl {
        onInit { /* code */ }

        onIntent { intent -> /* code */ }

        onDestroy { /* code */ }
    },
) {
    sealed interface Intent {
        data object DoCoolStuff : Intent
    }

    data class State(
        val value: Int = 0,
    )

    sealed interface SideEffect {
        data object SomethingHappened : SideEffect
    }
}

В этом примере для создания Actor используется dsl функция actorDsl. Данный dsl позволяет:

  • выполнить лямбду в момент инициализации Store, переданную в функцию onInit

  • выполнить лямбду в момент уничтожения Store, переданную в функцию onDestroy

  • обрабатывать каждый полученный Intent при помощи декларирования обработчика посредством вызова функции onIntent

Важно запомнить, что регистрировать обработчики можно только один раз, иначе библиотека выдаст исключение.

Заключение

Данной информации достаточно для первого знакомства с библиотекой. SimpleMVI доступен на maven central, так что вы без проблем можете подключить его к своим проектам

В следующей части статьи я погружусь в детали реализации и особенности библиотеки

Исходный код и примеры доступны в репозитории: SimpleMVI

P.S.

Буду рад всех видеть в своем Telegram канале

© Habrahabr.ru