Всем привет! В этой статье я хочу рассказать о новой библиотеке, которая переносит шаблон проектирования MVI в Android. Эта библиотека называется MVIDroid, она полностью написана на Kotlin, легка и использует RxJava 2.x. Автор библиотеки — лично я, ее исходный код доступен на GitHub, подключить ее можно через JitPack (ссылка на репозиторий в конце статьи).
Данная статья состоит из двух частей: общего описания библиотеки и примера ее использования.
МВИ
И так, в качестве предисловия, напомню, что такое МВИ вообще.Модель — Представление — Намерение или по-русски Модель — Представление — Намерение.
Это шаблон проектирования, в котором Модель является активным компонентом, который принимает намерения в качестве входных данных и создает состояния.
Представление, в свою очередь, принимает модели представления и создает те же намерения.
Состояние преобразуется в модель представления с помощью функции преобразователя (View Model Mapper).
Схематически шаблон MVI можно представить следующим образом:
В MVIDroid представление не создает намерения напрямую.
Вместо этого он создает события пользовательского интерфейса, которые затем преобразуются в намерения с помощью функции преобразователя.
Основные компоненты MVIDroid
Модель
Начнем с Модели.В библиотеке понятие Модели несколько расширено; здесь он производит не только Штаты, но и Ярлыки.
Теги используются для связи между моделями.
Метки некоторых моделей можно преобразовать в намерения других моделей с помощью функций преобразователя.
Схематически Модель можно представить следующим образом:
В 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 — это группа Моделей, объединенных общей целью.Например, вы можете выбрать все модели для экрана в качестве компонента.
Другими словами, Компонент является фасадом для заключенных в нем Моделей и позволяет скрыть детали реализации (Модели, функции-трансформеры и их связи).
Давайте посмотрим на диаграмму компонентов:
Как видно из схемы, компонент выполняет важную функцию преобразования и перенаправления событий.
Полный список функций компонента выглядит следующим образом:
- Связывает входящие события просмотра и метки с каждой моделью, используя предоставленные функции преобразователя.
- Выводит исходящие метки модели
- Уничтожает все модели и разрывает все соединения при уничтожении компонента.
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
-
Наслаждайтесь Бесплатными Онлайн-Флеш-Играми
19 Oct, 24 -
Конкурс Дудлов Для Google До 12 Апреля.
19 Oct, 24 -
Как Научиться Вставать По Будильнику
19 Oct, 24 -
Как Мтт Слил Свою Клиентскую Базу
19 Oct, 24