The elm architecture android

The Elm Architecture

The Elm Architecture is a pattern for architecting interactive programs, like webapps and games.

This architecture seems to emerge naturally in Elm. Rather than someone inventing it, early Elm programmers kept discovering the same basic patterns in their code. It was kind of spooky to see people ending up with well-architected code without planning ahead!

So The Elm Architecture is easy in Elm, but it is useful in any front-end project. In fact, projects like Redux have been inspired by The Elm Architecture, so you may have already seen derivatives of this pattern. Point is, even if you ultimately cannot use Elm at work yet, you will get a lot out of using Elm and internalizing this pattern.

The Basic Pattern

Elm programs always look something like this:

The Elm program produces HTML to show on screen, and then the computer sends back messages of what is going on. «They clicked a button!»

What happens within the Elm program though? It always breaks into three parts:

  • Model — the state of your application
  • View — a way to turn your state into HTML
  • Update — a way to update your state based on messages

These three concepts are the core of The Elm Architecture.

The next few examples are going to show how to use this pattern for user input, like buttons and text fields. It will make this much more concrete!

Follow Along

The examples are all available in the online editor:

This editor shows hints in the top left corner:

Be sure to try out the hints if you run into something confusing!

Источник

Пишем под android с Elmslie

Вступление

Это третья часть серии статей об архитектуре android приложения vivid.money. В ней мы расскажем об Elmslie — библиотеке для написания кода под android с использованияем ELM архитектуры. Мы назвали ее в честь Джорджа Эльмсли, шотландского архитектора. С сегодняшнего дня она доступна в open source. Это реализация TEA/ELM архитектуры на kotlin поддержкой android. В первой статье мы рассказали о том почему выбрали ELM. Перед прочтением этой статьи лучше ознакомиться как минимум со второй частью, в которой мы более подробно рассказывали том собственно такое ELM.

Оглавление

Что будем писать

В предыдущей статье мы рассказывали о том что такое ELM архитектура. В ней приводился пример, его и будем реализовывать. В нем происходит загрузка числового значения и есть возможность его обновлять. Исходный код для этого примера доступен на GitHub.

Схема работы экрана

Модель

Написание экрана проще начинать с проектирования моделей. Для каждого экрана нужны State, Effect, Command и Event. Рассмотрим каждый из них по очереди:

State

State описывает полное состояние экрана. В каждый момент времени по нему можно полностью восстановить то что сейчас показывается пользователю. Правильнее делать исключения, если это усложняет логику. Не нужно сохранять, показывается ли сейчас диалог на экране или что сейчас находится в каждом EditText.

На нашем экране будет отображаться либо числовое значение, либо состояние загрузки. Это можно задать двумя полями в классе: val isLoading: Boolean и val value: Int? . Для удобства изменения, State лучше реализовывать как data class. В итоге получается так:

Effect

Каждый Effect описывает side-effect в работе экрана. То есть это события, связанные с UI, происходящие ровно один раз, причем только когда экран виден пользователю. Например, это могут быть навигация, показ диалога или отображение ошибки.

В нашем примере единственной командой UI будет показ Snackbar при ошибке загрузки value. Для этого заведем Effect ShowError. Для удобства Effect можно создавать как sealed class, чтобы не забыть обработать новые добавленные эффекты:

Command

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

У нас будет одна операция — загрузить данные. Эту Command назовем LoadValue. Команды так же удобнее задавать как sealed class:

Event

События описываюся все что происходит пока работает экран. Ничего не может случиться без того, чтобы был получен новый Event. Каждое изменение State, Command или Effect могут быть вызваны только каким-нибудь событием/на состояние экрана и выполняемые операции. В нашем проекте мы разделяем события на две категории:

Читайте также:  Root android with adb shell

Event.UI: Все события, которые происходят во View слое. Такие как Жизненный цикл экрана или взаимодействие с пользователем

Event.Internal: результаты выполнения команд в Actor

Однако это раделение делать не обязательно, оно лишь упрощает понимание.

В этом примере будет два UI события: Init — открытие экрана и ReloadClick — нажатие на кнопку обновления значение. Internal события тоже два: ValueLoadingSuccess — успешный результат Command LoadValue и ValueLoadingError, которое будет отправляться при ошибке загрузки значения.

Если использовать разделение на UI и Internal, то Event удобнее задавать как иерархию sealed class:

Реализуем Store

Закончив с моделями, перейдем собственно к написанию кода. Сам Store реализовывать не нужно, он предоставляется библиотекой классом ElmStore .

Repository

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

Actor

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

Для его создания нужно реализовать интерфейс Actor , который предоставляется библиотекой. Actor получает на вход Command, а результатом его работы должен быть Observable , с событиями, которые сразу будут отправлены в Reducer. Для удобства в библиотеке есть функции mapEvents , mapSuccessEvent , mapErrorEvent и ignoreEvents , которые позволяют преобразовать данные в Event.

В нашем случае Actor будет выполнять только одну команду. При выполнении команды загрузки мы будем обращаться к репозиторию. В случае получения успешного значения будет оправляться событие ValueLoadingSuccess, а при ошибке ErrorLoadingValue. B итоге получается такая реализация:

Reducer

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

В этом классе нужно реализовать функцию reduce для обработки событий. Помимо вашей логики в Reducer можно использовать 3 функции:

state — позволяет изменить состояние экрана

effects — отправляет эффект во View

commands — запускает команду в Actor

Собираем Store

После того как написаны все компоненты нужно создать сам Store:

Экран

Для написания android приложений в elmslie есть отдельный модуль elmslie-android, в котором предоставляются классы ElmFragment и ElmAсtivity . Они упрощают использование библиотеки и имеют схожий вид. В них нужно реализовать несколько методов:

val initEvent: Event — событие инициализации экрана

fun createStore(): Store — создает Store

fun render(state: State) — отрисовывает State на экране

fun handleEffect(effect: Effect) — обрабатывает side Effect

В нашем примере получается такая реализация:

Заключение

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

Источник

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

Про проект

Всем привет! Меня зовут Даниил Климчук. Год назад я пришел в vivid.money третьим Android-разработчиком. Несмотря на это, в проекте практически не было кода, а первые фичи только начинали разрабатываться. Нам нужно было запустить новое банковское приложение в европе, где придется конкурировать с такими компаниями, как Revolut. Уже тогда было понятно, что команда очень быстро значительно вырастет. Конечно, стоило сразу задуматься о том, как будет развиваться архитектура проекта. Через год, когда проект запустится, на это не останется времени, а оправданий вносить значительные изменения просто не будет. Одним из ключевых решений на начальном этапе стал выбор архитектуры слоя представления. В этой статье я поделюсь тем, как мы его принимали.

Оглавление

Про выбор

Возможные подходы для нас явно разделились на две группы: проверенные временем и надежные MVP, MVVM и MVC, а также новые архитектуры, использующие Unidirectional Data Flow: Redux, MVI, Elm (aka MVU) и т.д.. Не хотелось сравнивать каждые в отдельности, а для упрощения определиться в какую сторону смотреть в первую очередь. Поэтому быстро набросали список требований.

Читайте также:  Как называются разъемы для зарядки андроид

Код был поддерживаемым

Лучше помнить про то, что с прошествием времени код все еще нужно будет понимать и менять.

Новые люди могли быстро влиться

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

Печатать одно и то же на каждом экране утомительно и в добавок может привести к ошибкам.

Был единый подход к архитектуре

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

Было проще покрыть тестами

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

Исходя из этого принять решение сразу было достаточно сложно. Пришлось обратиться к проверенному методу и расписать плюсы и минусы обоих подходов:

За старое доброе

Достаточно реализации базовых классов MVP, после этого на каждый экран нужно создавать только Presenter/ViewModel/Controller. В отличие от UDF архитектур, для которых даже каждое событие требует своего класса.

Это всем известные архитектуры

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

Проще code review

При изменении экрана меняется только Presenter и View. В UDF архитектурах логика из Presenter разбивается на несколько классов, каждый из которых приходится просматривать в отдельности.

Нет проблемы SingleLiveEvent

Проблема описана в issue для android architecture components. В MVP в принципе отсутствует, а в MVVM с LiveData можно использовать собственно сам класс SingleLiveEvent. Для UDF архитектур нет устоявшегося подхода с решением этой проблемы, для нее придется придумывать что-то свое.

Простота в понимании

Если рассматривать саму архитектуру, то MVP и MVVM определяют только наличие двух классов View и Presenter (или соответственно ViewModel). В UDF архитектурах структура более сложная и у их составляющих более узкая зона ответственности.

За новое хайповое

Собственно сам UDF

В таких архитектурах есть только один фиксированный путь, по которому данные передаются в приложении. В отличие например MVP, где в Presenter со временем может накапливаться огромное количество спагетти-кода, который со временем становится сложно понимать.

Single immutable state

Состояние экрана выделяется в отдельный класс, который называется State. Если нет такого явного ограничения, состояние может описываться множеством флагов, иногда частично находится где-то во View или дублируется в нескольких местах. Такой подход позволяет иметь single source of truth о текущем состоянии экрана. Важным достоинством этого подхода является возможность в каждый момент времени обратиться с State и понять, например, идет ли сейчас загрузка данных.

Обработка смены конфигурации и восстановления процесса

Намного проще, поскольку есть single state. Достаточно просто отрисовать его заново на экране, чтобы полностью восстановить предыдущее состояние. При обработке смерти процесса есть необходимость сохранить только единственный класс. Справедливости ради, например, использование LiveDatа позволит обработать смену конфигурации. Однако это дополнительная зависимость, которую придется тянуть в проект. Также, стандартный механизм обработки смерти процесса для ViewModel на основе SavedStateHandle намного сложнее в реализации и усложняет логику во ViewModel.

Логика слоя представления разделена на несколько классов, каждый из которых выполняет свою функцию. В отличие, например, от MVP в котором все логика находится в Presenter. Получается, что он отвечает за обработку изменения состояния, загрузку данных, изменение модели итд. Явного разделения на зоны ответственности нет и часто она вся находится в одном классе.

Не нужно думать о потокобезопасности, вся синхронизация происходит на уровне реализации архитектуры. Из-за разделения ответственности и неизменяемого состояния различные части кода не должны обращаться к одним и тем же изменяемым данным. Например в MVP в рамках Presenter намного проще выстрелить себе в ногу, случайно поменяв какой-то флаг в состоянии не с главного потока.

Проблема bloated presenter

Со временем Presenter или ViewModel может вырасти до нескольких тысяч строк кода. В этот момент придется думать о том как разделять логику, что может вполне вылиться в решение, менее гибкое, чем изначально заложенная UDF архитектура.

В некоторых UDF архитектурах есть возможность составлять экран из нескольких частей вместо одного большого Presenter. Например в MviCore есть разделение на Feature, а в ELM — компоненты. Каждая из них написана в одном стиле и вместе они составляют логику экрана. Вдобавок эти части можно переиспользовать, в отличие MVP и MVVM, где придется придумывать свое нестандартное решения этой проблемы.

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

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

Заставляет планировать логику экрана заранее

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

Возможность реализовать Time Travel Debug

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

UDF архитектуры лучше подходят для работы с Jetpack Compose, для которого недавно уже вышла alpha версия. UDF архитектуры имеют единый метод для отрисовки состояния, которое сразу можно преобразовать в иерархию View.

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

Как принимали решение

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

MVI vs ELM

Многие реализации UDF архитектур сильно похожи, поэтому выделили основное различие: в MVI логика экрана разделена между Reducer и Intent, а в ELM полностью находится в Update.

MVI vs ELM

Например, при нажатии на кнопку загрузки, в MVI Intent знает про то, что нужно получить данные, а Reducer отвечает за то, чтобы показать состояние загрузки. В Elm за все это отвечает один класс Update, и только само получение данных происходит в рамках Side Effect.

Почему выбрали ELM

Решили руководствоваться уже существующими недостатками UDF архитектур, в которых были различия. Победил однозначно Elm:

Elm позволяет покрыть тестами всю логику экрана, написав тесты всего на один класс. При этом этот класс не содержит асинхронного кода и писать тесты значительно легче. Более сложные сценарии будут покрываться ui тестами, а работа по написанию unit тестов значительно сократится.

Понимание новыми членами команды

Человеку, который только что пришел работать Elm проще объяснить: «вот здесь логика, а вот здесь асинхронные операции». В отличии от MVI, в котором приходится представлять как все работает в целом.

Update из Elm можно рассматривать отдельно, поскольку в нем содержится вся логика. При code review кода, написанного на mvi, приходится больше переключаться между Intent и Reducer, потому что логика разделена между ними.

На текущий момент уже есть несколько open-source реализаций Elm архитектуры, например Teapot, Puerh и Elmo, однако мы решили сделать свою.

Как решить проблемы UDF

Остались нерешенными еще два пункта, по ним пришлось искать решения.

Наша реализация с итоговым неймингом

Boilerplate

Головной болью таких подходов является создание большого числа классов на этапе создания экрана. Например, в нашей реализации это Actor, Reducer, State, Event, Effect, Command и StoreFactory. Простой экран с одним запросом превращается в долгое печатание давно заученного наизусть кода. Для решения этой проблемы был реализован плагин для Android Studio. Весь повторяющийся код можно сгенерировать и добавить новый экран становится не сложнее чем в привычном MVP.

SingleLiveEvent

Мы поддержали решение этой проблемы на уровней нашей реализации Elm. Мы выделили отдельную сущность для сообщений, которые должны быть переданы во View только один раз. Для них выделили отдельную очередь сообщений, на изменения в которой и подписывается View. На схеме эта сущность обозначена как Effect.

Восстановление состояния

Эту проблему можно разделить на две части: восстановление состояния при смене конфигурации и при восстановлении процесса. Для решения первой проблемы хватает хранения Elm компонента внутри Dagger Scope. Новый инстанс фрагмента подключится к компоненту и при инициализации получит последнее состояние. Чуть более сложной получилась обработка смерти процесса. По скольку есть выделенное в отдельный класс состояние, достаточно сохранить его в onSaveInstanceState.

А что дальше

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

Источник

Оцените статью