Mvidroid: Обзор Новой Библиотеки Mvi (Model-View-Intent)

Всем привет! В этой статье я хочу рассказать о новой библиотеке, которая переносит шаблон проектирования MVI в Android. Эта библиотека называется MVIDroid, она полностью написана на Kotlin, легка и использует RxJava 2.x. Автор библиотеки — лично я, ее исходный код доступен на GitHub, подключить ее можно через JitPack (ссылка на репозиторий в конце статьи).

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



МВИ

И так, в качестве предисловия, напомню, что такое МВИ вообще.

Модель — Представление — Намерение или по-русски Модель — Представление — Намерение.

Это шаблон проектирования, в котором Модель является активным компонентом, который принимает намерения в качестве входных данных и создает состояния.

Представление, в свою очередь, принимает модели представления и создает те же намерения.

Состояние преобразуется в модель представления с помощью функции преобразователя (View Model Mapper).

Схематически шаблон MVI можно представить следующим образом:

MVIDroid: обзор новой библиотеки MVI (Model-View-Intent)

В MVIDroid представление не создает намерения напрямую.

Вместо этого он создает события пользовательского интерфейса, которые затем преобразуются в намерения с помощью функции преобразователя.



MVIDroid: обзор новой библиотеки MVI (Model-View-Intent)



Основные компоненты MVIDroid



Модель

Начнем с Модели.

В библиотеке понятие Модели несколько расширено; здесь он производит не только Штаты, но и Ярлыки.

Теги используются для связи между моделями.

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

Схематически Модель можно представить следующим образом:

MVIDroid: обзор новой библиотеки MVI (Model-View-Intent)

В MVIDroid Модель представлена интерфейсом MviStore (название Store заимствовано из Redux):

  
  
  
  
  
  
  
  
  
  
  
   

interface MviStore<State : Any, in Intent : Any, Label : Any> : (Intent) -> Unit, Disposable { @get:MainThread val state: State val states: Observable<State> val labels: Observable<Label> @MainThread override fun invoke(intent: Intent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }

И так что мы имеем:
  • Интерфейс имеет три универсальных параметра: State — тип состояния, Intent — тип намерения и Label — тип метки.

  • Содержит три поля: состояние — текущее состояние Модели, состояния — Observable State и labels — Observable of Labels. Последние два поля позволяют подписаться на изменения Статуса и Меток соответственно.

  • Является потребителем намерений
  • Является одноразовым, что дает возможность уничтожить Модель и остановить все происходящие в ней процессы.

Обратите внимание, что все методы модели должны выполняться в основном потоке.

То же самое справедливо и для любого другого компонента.

Конечно, вы можете выполнять фоновые задачи, используя стандартные инструменты RxJava.

Компонент

Компонент в MVIDroid — это группа Моделей, объединенных общей целью.

Например, вы можете выбрать все модели для экрана в качестве компонента.

Другими словами, Компонент является фасадом для заключенных в нем Моделей и позволяет скрыть детали реализации (Модели, функции-трансформеры и их связи).

Давайте посмотрим на диаграмму компонентов:

MVIDroid: обзор новой библиотеки MVI (Model-View-Intent)

Как видно из схемы, компонент выполняет важную функцию преобразования и перенаправления событий.

Полный список функций компонента выглядит следующим образом:

  • Связывает входящие события просмотра и метки с каждой моделью, используя предоставленные функции преобразователя.

  • Выводит исходящие метки модели
  • Уничтожает все модели и разрывает все соединения при уничтожении компонента.

Компонент также имеет собственный интерфейс:

interface MviComponent<in UiEvent : Any, out States : Any> : (UiEvent) -> Unit, Disposable { @get:MainThread val states: States @MainThread override fun invoke(event: UiEvent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }

Давайте подробнее рассмотрим интерфейс компонента:
  • Содержит два универсальных параметра: UiEvent — тип событий просмотра и States — тип состояний модели.

  • Содержит поле состояний, которое предоставляет доступ к группе «Состояния модели» (например, в виде интерфейса или класса данных).

  • Является потребителем View Events
  • Является одноразовым, что позволяет уничтожить Компонент и все его Модели.



Вид

Как нетрудно догадаться, представление необходимо для отображения данных.

Данные для каждого представления группируются в модель представления и обычно представляются как класс данных (Kotlin).

Давайте посмотрим на интерфейс View:

interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable }

Здесь все немного проще.

Два универсальных параметра: ViewModel — тип модели представления и UiEvent — тип событий представления.

Одно поле uiEvents — это Observable для событий просмотра, позволяющее клиентам подписываться на эти же события.

И один метод subscribe(), который позволяет подписаться на модели просмотра.



Пример использования

Сейчас самое время попробовать что-то по-настоящему.

Я предлагаю сделать что-то очень простое.

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

Пусть это будет генератор UUID: по нажатию кнопки мы сгенерируем UUID и выведем его на экран.



Производительность

Сначала опишем модель представления:

data class ViewModel(val text: String)

И просмотреть события:

sealed class UiEvent { object OnGenerateClick: UiEvent() }

Теперь давайте реализуем само представление; для этого нам понадобится наследование от абстрактного класса MviAbstractView:

class View(activity: Activity) : MviAbstractView<ViewModel, UiEvent>() { private val textView = activity.findViewById<TextView>(R.id.text) init { activity.findViewById<Button>(R.id.button).

setOnClickListener { dispatch(UiEvent.OnGenerateClick) } } override fun subscribe(models: Observable<ViewModel>): Disposable = models.map(ViewModel::text).

distinctUntilChanged().

subscribe { textView.text = it } }

Все предельно просто: подписываемся на изменения UUID и обновляем TextView при получении нового UUID, а при нажатии кнопки отправляем событие OnGenerateClick.

Модель

Модель будет состоять из двух частей: интерфейса и реализации.

Интерфейс:

interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } }

Здесь все просто: наш интерфейс расширяет интерфейс MviStore за счет указания типов State и Intent. Тип меток — Ничего, поскольку наша Модель их не производит. Интерфейс также содержит классы State и Intent. Чтобы реализовать Модель, необходимо понять, как она работает. На вход модели поступают намерения, которые преобразуются в действия с помощью специальной функции IntentToAction. Действия передаются в Исполнитель, который выполняет их и создает Результат и Метку.

Результаты затем передаются в Редюсер, который преобразует текущее состояние в новое.

Все четыре компонента Модели:

  • IntentToAction — функция, преобразующая намерения в действия.

  • MviExecutor — выполняет действия и выдает результаты и метки.

  • MviReducer — преобразует пары (Состояние, Результат) в новые состояния.

  • MviBootstrapper — специальный компонент, позволяющий инициализировать Модель.

    Он производит все те же Действия, которые также передаются Исполнителю.

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

    Bootstrapper запускается автоматически при создании модели.

Для создания самой Модели необходимо использовать специальную Фабрику Моделей.

Он представлен интерфейсом MviStoreFactory и его реализацией MviDefaultStoreFactory. Фабрика принимает компоненты Модели и производит готовую Модель.

Наша фабрика моделей будет выглядеть так:

class UuidStoreFactory(private val factory: MviStoreFactory) { fun create(factory: MviStoreFactory): UuidStore = object : UuidStore, MviStore<State, Intent, Nothing> by factory.create( initialState = State(), bootstrapper = Bootstrapper, intentToAction = { when (it) { Intent.Generate -> Action.Generate } }, executor = Executor(), reducer = Reducer ) { } private sealed class Action { object Generate : Action() } private sealed class Result { class Uuid(val uuid: String) : Result() } private object Bootstrapper : MviBootstrapper<Action> { override fun bootstrap(dispatch: (Action) -> Unit): Disposable? { dispatch(Action.Generate) return null } } private class Executor : MviExecutor<State, Action, Result, Nothing>() { override fun invoke(action: Action): Disposable? { dispatch(Result.Uuid(UUID.randomUUID().

toString())) return null } } private object Reducer : MviReducer<State, Result> { override fun State.reduce(result: Result): State = when (result) { is Result.Uuid -> copy(uuid = result.uuid) } } }

В этом примере представлены все четыре компонента Модели.

Сначала идет метод создания фабрики, затем Действия и Результаты, затем Исполнитель и в самом конце Редуктор.



Компонент

Состояния Компонента (группы состояний) будем описывать классом данных:

data class States(val uuidStates: Observable<UuidStore.State>)

При добавлении новых моделей в компонент их состояния также следует добавлять в группу.

И, собственно, сама реализация:

class Component(uuidStore: UuidStore) : MviAbstractComponent<UiEvent, States>( stores = listOf( MviStoreBundle( store = uuidStore, uiEventTransformer = UuidStoreUiEventTransformer ) ) ) { override val states: States = States(uuidStore.states) private object UuidStoreUiEventTransformer : (UiEvent) -> UuidStore.Intent? { override fun invoke(event: UiEvent): UuidStore.Intent? = when (event) { UiEvent.OnGenerateClick -> UuidStore.Intent.Generate } } }

Мы унаследовали абстрактный класс MviAbstractComponent, указали типы состояний представления и событий, передали нашу модель суперклассу и реализовали поле состояний.

Кроме того, мы создали функцию-трансформер, которая преобразует события просмотра в намерения нашей модели.



Сопоставление моделей представления

Состояния и Модель представления у нас есть, пора трансформировать одно в другое.

Для этого реализуем интерфейс MviViewModelMapper:

object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } }



Связывание

Иметь компонент и представление самих по себе недостаточно.

Чтобы всё начало работать, их нужно соединить.

Пришло время создать Activity:

class UuidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uuid) bind( Component(UuidStoreFactory(MviDefaultStoreFactory).

create()), View(this) using ViewModelMapper ) } }

Мы использовали метод привязки(), который принимает компонент и массив представлений с их преобразователями модели.

Этот метод является методом расширения LifecycleOwner (которые являются Activity и Fragment) и использует DefaultLifecycleObserver из пакета Arch, который требует совместимости с исходным кодом Java 8. Если по каким-то причинам вы не можете использовать Java 8, то вам подойдет второй метод bind(), который не является методом расширения и возвращает MviLifecyleObserver. В этом случае вам придется вызывать методы жизненного цикла самостоятельно.



Ссылки

Исходный код библиотеки, а также подробную инструкцию по ее подключению и использованию можно найти по адресу GitHub .

Теги: #разработка для Android #mvi #mvi #pattern #architecture #pattern #architecture #Android development

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.