[Программирование, Проектирование и рефакторинг, Разработка под Android, Kotlin] Пишем под android с Elmslie

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
20-Апр-2021 12:30

ВступлениеЭто третья часть серии статей об архитектуре android приложения vivid.money. В ней мы расскажем об Elmslie - библиотеке для написания кода под android с использованияем ELM архитектуры. Мы назвали ее в честь Джорджа Эльмсли, шотландского архитектора. С сегодняшнего дня она доступна в open source. Это реализация TEA/ELM архитектуры на kotlin поддержкой android. В первой статье мы рассказали о том почему выбрали ELM. Перед прочтением этой статьи лучше ознакомиться как минимум со второй частью, в которой мы более подробно рассказывали том собственно такое ELM.Оглавление
Что будем писатьВ предыдущей статье мы рассказывали о том что такое ELM архитектура. В ней приводился пример, его и будем реализовывать. В нем происходит загрузка числового значения и есть возможность его обновлять. Исходный код для этого примера доступен на GitHub.
МодельНаписание экрана проще начинать с проектирования моделей. Для каждого экрана нужны State, Effect, Command и Event. Рассмотрим каждый из них по очереди:StateState описывает полное состояние экрана. В каждый момент времени по нему можно полностью восстановить то что сейчас показывается пользователю. Правильнее делать исключения, если это усложняет логику. Не нужно сохранять, показывается ли сейчас диалог на экране или что сейчас находится в каждом EditText.На нашем экране будет отображаться либо числовое значение, либо состояние загрузки. Это можно задать двумя полями в классе: val isLoading: Boolean и val value: Int?. Для удобства изменения, State лучше реализовывать как data class. В итоге получается так:
data class State(
  val isLoading: Boolean = false,
  val value: Int? = null
)
EffectКаждый Effect описывает side-effect в работе экрана. То есть это события, связанные с UI, происходящие ровно один раз, причем только когда экран виден пользователю. Например, это могут быть навигация, показ диалога или отображение ошибки.В нашем примере единственной командой UI будет показ Snackbar при ошибке загрузки value. Для этого заведем Effect ShowError. Для удобства Effect можно создавать как sealed class, чтобы не забыть обработать новые добавленные эффекты:
sealed class Effect {
  object ShowError : Effect()
}
CommandКаждая Command обозначает одну асинхронную операцию. Подробнее о том как они обрабатываются расскажем чуть позже. В результате выполнения команды получаются события, которые в свою очередь повлияют на состояние экрана.У нас будет одна операция - загрузить данные. Эту Command назовем LoadValue. Команды так же удобнее задавать как sealed class:
sealed class Command {
  object LoadValue : Command()
}
EventВсе события, которые влияют на состояние и действия на экране: Ui: ЖЦ экрана, взаимодействие с пользователем, все что приходит из View слоя Internal: Результаты операций с бизнес логикойТеперь перейдем к событиям. В нашем проекте мы разделяем события на две категории:
  • Event.UI: все события, которые происходят во View слое
  • Event.Internal: результаты выполнения команд в Actor.
В этом примере будет два UI события: Init - открытие экрана и ReloadClick - нажатие на кнопку обновления значение. Internal события тоже два: ValueLoadingSuccess - успешный результат Command LoadValue и ValueLoadingError, которое будет отправляться при ошибке загрузки значения.Если использовать разделение на UI и Internal, то Event удобнее задавать как иерархию sealed class:
sealed class Event {
  sealed class Ui : Event() {
    object Init : Ui()
    object ReloadClick : Ui()
  }
  sealed class Internal : Event() {
    data class ValueLoadingSuccess(val value: Int) : Internal()
    object ValueLoadingError : Internal()
  }
}
Реализуем StoreЗакончив с моделями, перейдем собственно к написанию кода. Сам Store реализовывать не нужно, он предоставляется библиотекой классом ElmStore.RepositoryДля нашего примера напишем симуляцию работы с моделью, которая будет возвращать либо случайный Int, либо ошибку:
object ValueRepository {
private val random = Random()
fun getValue() = Single.timer(2, TimeUnit.SECONDS)
    .map { random.nextInt() }
    .doOnSuccess { if (it % 3 == 0) error("Simulate unexpected error") }
}
ActorActor - место, в котором выполняются долгосрочные операции на экране - запросы в сеть, подписка на обновление данных, итд. В большинстве случаев, как и в нашем примере просто маппит результаты запросов в Event.Для его создания нужно реализовать интерфейс Actor, который предоставляется библиотекой. Actor получает на вход Command, а результатом его работы должен быть Observable<Event>, с событиями, которые сразу будут отправлены в Reducer. Для удобства в библиотеке есть функции mapEvents, mapSuccessEvent, mapErrorEvent и ignoreEvents, которые позволяют преобразовать данные в Event.В нашем случае Actor будет выполнять только одну команду. При выполнении команды загрузки мы будем обращаться к репозиторию. В случае получения успешного значения будет оправляться событие ValueLoaded, а при ошибке ErrorLoadingValue. B итоге получается такая реализация:
class Actor : Actor<Command, Event> {
override fun execute(command: Command): Observable<Event> = when (command) {
    is Command.LoadNewValue -> ValueRepository.getValue()
        .mapEvents(Internal::ValueLoaded, Internal.ErrorLoadingValue)
}
}
ReducerВ этом классе содержится вся логика работы экрана и только она. Это позволяет в одном месте увидеть все что происходит на экране, не отвлекаясь на детали реализации. Так же для него удобно писать тесты, поскольку он не содержит многопоточного кода и представляет из себя чистую функцию. В нем на основании предыдущего состояния экрана и нового события рассчитывается новое состояние экрана, команды и эффекты.В этом классе нужно реализовать функцию reduce для обработки событий. Помимо вашей логики в Reducer можно использовать 3 функции:
  • state - позволяет изменить состояние экрана
  • effects - отправляет эффект во View
  • commands - запускает команду в Actor
class Reducer : DslReducer<Event, State, Effect, Command>() {
override fun Result.reducer(event: Event) = when (event) {
    is Internal.ValueLoaded -> {
        state { copy(isLoading = false, value = event.value) }
    }
    is Internal.ErrorLoadingValue -> {
        state { copy(isLoading = false) }
        effects { +Effect.ShowError }
    }
    is Ui.Init -> {
        state { copy(isLoading = true) }
        commands { +Command.LoadNewValue }
    }
    is Ui.ClickReload -> {
        state { copy(isLoading = true, value = null) }
        commands { +Command.LoadNewValue }
    }
}
}
Собираем StoreПосле того как написаны все компоненты нужно создать сам Store:
fun storeFactory() = ElmStore(
    initialState = State(),
    reducer = MyReducer(),
    actor = MyActor()
).start()
ЭкранДля написания 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
В нашем примере получается такая реализация:
class MainActivity : ElmActivity<Event, Effect, State>() {
override val initEvent: Event = Event.Ui.Init
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    findViewById<Button>(R.id.reload).setOnClickListener {
       store.accept(Event.Ui.ClickReload)
    }
}
override fun createStore() = storeFactory()
override fun render(state: State) {
    findViewById<TextView>(R.id.currentValue).text = when {
        state.isLoading -> "Loading..."
        state.value == null -> "Value = Unknown"
        else -> "Value = ${state.value}"
    }
}
override fun handleEffect(effect: Effect) = when (effect) {
    Effect.ShowError -> Snackbar
        .make(findViewById(R.id.content), "Error!", Snackbar.LENGTH_SHORT)
        .show()
}
}
ЗаключениеВ нашей библиотеке мы постарались реализовать максимально простой подход к ELM архитектуре, который был бы максимально удобен в использовании. По ощущениям нашим разработчиков, впервые сталкивающихся с ELM, порог входа в библиотеку невысокий. Также мы постарались сильно облегчить написание кода с помощью кодогенерации. Саму библиотеку мы без проблем используем уже около года в продакшене и полностью ей довольны. Будем рады фидбеку и надеемся, что она пригодится и вам.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_proektirovanie_i_refaktoring (Проектирование и рефакторинг), #_razrabotka_pod_android (Разработка под Android), #_kotlin, #_elm, #_mvi, #_arhitektura (архитектура), #_vivid.money, #_udf, #_arhitektura_prilozhenij (архитектура приложений), #_mobilnaja_razrabotka (мобильная разработка), #_android, #_kotlin, #_arhitektura_po (архитектура по), #_blog_kompanii_vivid_money (
Блог компании Vivid Money
)
, #_programmirovanie (
Программирование
)
, #_proektirovanie_i_refaktoring (
Проектирование и рефакторинг
)
, #_razrabotka_pod_android (
Разработка под Android
)
, #_kotlin
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 22-Ноя 04:02
Часовой пояс: UTC + 5