Pull to refresh
0

Разбираем ELM архитектуру в рамках мобильного приложения

Reading time 6 min
Views 13K

Это вторая часть серии статей об архитектуре android приложения vivid.money. В ней мы расскажем в деталях о том, что из себя представляет ELM архитектура. В связи с тем, что наша реализация доступна в open source в качестве библиотеки Elmslie, в статье будет использоваться нейминг из нее.

Оглавление

Вступление

Одной из основных проблем в разработке мобильных приложений с использованием MVP/MVVM/MVC паттернов становится раздутие презентеров. Часто в них скапливается абсолютно все управление асинхронной работой и состоянием приложения. С течением времени, усложнением логики и общим ростом кодовой базы их становится невероятно трудно менять. А понимание того что происходит в написанном другим разработчиком презентере может стать непосильной задачей и исправление багов лишь привносит больше новых ошибок.

С этой задачей призваны были справиться Unidirectional Data Flow архитектуры. Первое решение для android было описано уже больше 4х лет назад (!) Ханнесом Дорфманом в статье об MVI на андроид. Помимо MVI, самого популярного представителя Unidirectional архитектур в мобильном сообществе, существуют и другие. В рамках этой статьи остановимся на архитектуре, которая используется у нас - ELM.

О чем эта статья?

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

Место ELM в архитектуре
Место ELM в архитектуре

Представьте, что мы проектируем архитектуру с нуля. Конечно, на момент написания статьи результат уже известен. В каждый момент времени можно было бы принять и другое решение, выделить другие детали или решить проблему по-другому. Такой способ изложения выбрал исключительно для упрощения понимания.

В описании мы не будем останавливаться на модели, рассказывать о том как писать бизнес-логику, делать запросы к API. Про организацию кода во View тоже не будет ни слова, эти моменты остаются на ваше усмотрение.

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

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

Основной составляющей ELM является одна сущность - Store. В простом варианте весь слой представления описывается одной сущностью. В более сложных случаях экран может состоять из нескольких Store, но об этом расскажем в одной из следующих статей. Попробуем описать свойства Store:

Требования к слою представления
Требования к слою представления

Любой слой представления должен общаться с моделью. Нам придется обращаться с бизнес логикой, делать запросы к Api, сохранять данные в кеш и так далее. Представим это на диаграмме как возможность Store обращаться к модели, получать из нее данные и запускать операции.

Взаимодействие с View устроено немного сложнее. У него есть три составляющие:

  • В пользовательском интерфейсе происходят события - нажатия на кнопку, прокрутка списка, pull-to-refresh и другие. Назовем их Event.UI.

  • У экрана есть некоторое состояние. В него может входить информация о том, показывается ли сейчас состояние загрузки, данные для отображения в списке или текущее положение toggle switch. Все это включим в термин State.

  • Слой представления не только имеет состояние, но еще и может отдавать команды View. Например нужна возможность показать Toast, Snackbar или перейти на другой экран. Все это не получится описать в State, поскольку в нем хранится информация, а требуется представить некоторое действие. Для этого выделим отдельную сущность - Effect.

А теперь внутри

В предыдущем разделе мы описали поведение Store снаружи, то как оно выглядит для внешнего наблюдателя или пользователя. Теперь попробуем описать то, что происходит внутри него:

  • Обработка событий в пользовательском интерфейсе (Event.UI)

  • Изменение состояния экрана (State)

  • Запуск операций в UI (Effect)

  • Получение данных из модели

  • Запуск операций в модели

  • Сложные вычисления

Все эти вещи можно разделить на две группы, которые мы объединили в две сущности - Actor и Reducer

Разделение работы экрана
Разделение работы экрана

Опишем сущности, которые у нас получились:

Actor

Схема работы Actor
Схема работы Actor



В Actor находятся все асинхронные операции, вычисления и работа с моделью. Опишем это с помощью Command, которая в общем случае запускает некоторую операцию и Event, который вернет результат этой операции.

Например:

  • Подписка на обновление данных в модели

  • Выполнение запроса к API

  • Запуск таймера на выполнение операции

Reducer

Схема работы Reducer
Схема работы Reducer

По сути в Reducer осталась вся логика работы экрана. Он знает о текущем состоянии экрана, узнает о происходящих событиях и вычисляет реакцию на них. События могут приходить из UI и из Actor, как результат работы операции. Реакция состоит из Effect - команды для UI, State текущего состояния для отрисовки на экране и Command - запуска операции в Actor

Например:

  • При Event - нажатие на кнопку загрузки в State выставится флаг isLoading на true и запустится Command - сделать запрос к API

  • При Event - произошла ошибка при загрузке данных в State выставится флаг isLoading в false и отправится Effect - показать ошибку в UI

Отличным качеством Reducer является то, что его можно реализовать не используя асинхронных операций. Его можно представить как pure function. То есть функцией, которая не создает побочных эффектов и всегда выдает одинаковый результат для одних и тех же входных данных.

Result

Комапоненты Result
Комапоненты Result





Выделим так же отдельную сущность, которая будет представлять ту самую реакцию Reducer на Event и назовем ее Result.

Она состоит из:

  • Effect - команды для UI

  • State - текущее состояния экрана

  • Command - команды запуска операций в Actor

Собираем все вместе

Если объединить все эти компоненты получится примерно следующая картина:

Внутренности Store
Внутренности Store

View и Actor являются источниками событий. Это представлено в виде Event. События разделяются по типу источника, для View это Event.Ui, а для Actor это Event.Internal. События побуждают изменения состояния экрана, одиночные эффекты, а также запуск асинхронных операций. Состояние экрана представлено State, которое доставляется View для отрисовки. Одиночные эффекты обозначены как Effect и так же обрабатываются View. Actor в свою очередь работает с моделью, запускает операции и получает из нее данные. А Store связывает все это вместе.

Как это работает?

Далее на GIF диаграммах схематично представлена работа простого экрана. Слева - UI, в центре то что происходит в ELM, справа - текущий State экрана.

Сценарий успешной загрузки

Разберем сценарий, когда при нажатии на кнопку значение успешно загружается и отображается в UI.

Работа ELM во время успешного сценария
Работа ELM во время успешного сценария
  • Пользователь нажимает на кнопку Reload

  • UI отправляет Event.UI обозначенный CLICK

  • CLICK приходит в Reducer

  • Результатом работы Reducer становится изменение isLoading на true в State и отправка Command обозначенная как LOAD

  • Из-за изменения State в UI отрисовывается текст LOADING...

  • В Actor выполняется Command загрузки данных - LOAD

  • Результатом выполнения команды становится Event.Internal со значением VALUE

  • Reducer обрабатывает событие VALUE и изменяет в State у поле value значение на 123, а у поля isLoading на false

  • В UI отрисовывается текст VALUE = 123
    Сценарий неуспешной загрузки

А теперь неуспешной

Разберем сценарий, когда при нажатии на кнопку значение не получается загрузить, после чего показывается Snackbar с ошибкой.

Работа ELM в случае ошибки
Работа ELM в случае ошибки
  • Пользователь снова нажимает на кнопку Reload и отправляется Event.UI обозначенный CLICK

  • CLICK приходит в Reducer

  • Результатом работы Reducer становится изменение isLoading на true в State и отправка Command обозначенная как LOAD

  • Из-за изменения State в UI отрисовывается текст LOADING...

  • В Actor выполняется Command загрузки данных - LOAD

  • Результатом выполнения команды становится Event.Internal со значением ERROR

  • Reducer обрабатывает событие ERROR и изменяет в State значение у поля isLoading на false, а также отправляет Effect под названием ERROR

  • UI обрабатывает Effect обозначенный ERROR и показывает Snackbar с ошибкой

В итоге

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

Поскольку существующие реализации ELM показались нам недостаточно лаконичными и простыми в использовании мы создали Elmslie. Мы постарались вобрать достоинства существующих реализаций, максимально упростив написание кода. В следующей части мы расскажем о том, как пользоваться нашей библиотекой.

Tags:
Hubs:
+2
Comments 9
Comments Comments 9

Articles

Information

Website
vivid.money
Registered
Founded
Employees
201–500 employees
Location
Германия