company_banner

Архитектура EBA aka реактивность на всю катушку

    Я пришел в Tinkoff пару лет назад, на новый проект Клиенты и проекты, который тогда только запускался.
    Сейчас уже не помню своих ощущений от новой тогда для меня архитектуры. Но точно помню: было непривычно, что Rx используется еще где-то, за пределами обычных походов в сеть и в базу. Сейчас, когда эта архитектура уже прошла некоторый эволюционный путь развития, хочется наконец рассказать о том, что было и к чему пришло.



    По моему мнению, все популярные ныне архитектуры — MVP, MVVM и даже MVI — уже давно на арене и не всегда заслуженно. Разве у них нет недостатков? Я вижу их немало. Мы у себя решили, что хватит это терпеть, и (пере)изобрели новую, максимально асинхронную архитектуру.


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


    • Огромные Presenter/ViewModel.
    • Огромное количество switch-case в MVI.
    • Невозможность переиспользовать части Presenter/ViewModel и, как следствие, необходимость дублировать код.
    • Кучи мутабельных переменных, которые можно модифицировать откуда угодно. Соответственно, такой код сложно поддерживать и изменять.
    • Не декомпозированное обновление экрана.
    • Сложно писать тесты.

    Проблематика


    В каждый момент времени у приложения есть определенное состояние, которое задает его поведение и то, что видит пользователь. Это состояние включает в себя все значения переменных — от простых флагов до отдельных объектов. Каждая из этих переменных живет своей жизнью и управляется различными частями кода. Определить текущее состояние приложения можно, лишь проверив их все, одну за другой.
    Статья о современной MVI-архитектуре на Kotlin


    Глава 1. Эволюция — наше всё


    Изначально мы писали на MVP, но немного мутированной. Это была какая-то смесь MVP и MVI. Были сущности из MVP в виде презентера и интерфейса View:


    interface NewTaskView {
       val newTaskAction: Observable<NewTaskAction>
       val taskNameChangeAction: Observable<String>
       val onChangeState: Consumer<SomeViewState>
    }

    Уже тут можно заметить подвох: View здесь очень далека от канонов MVP. В презентере был метод:


    fun bind(view: SomeView): Disposable

    Снаружи передавалась реализация интерфейса, которая реактивно подписывалась на изменения UI. И это уже попахивает MVI!


    Дальше — больше. В Presenter’e создавались и подписывались на изменения View разные интеракторы, но они не вызывали методы UI напрямую, а возвращали некоторый глобальный State, в котором были все возможные состояния экрана:


    compositeDisposable.add(
        Observable.merge(firstAction, secondAction)
                   .observeOn(AndroidSchedulers.mainThread())
                   .subscribe(view.onChangeState))
    return compositeDisposable

    class SomeViewState(val progress: Boolean? = null,
                                val error: Throwable? = null,
                                val errorMessage: String? = error?.message,
                                val result: TaskUi? = null)
    

    Активити была наследником интерфейса SomeViewStateMachine:


    interface SomeViewStateMachine {
       fun toSuccess(task: SomeUiModel)
       fun toError(error: String?)
       fun toProgress()
       fun changeSomeButton(buttonEnabled: Boolean)
    }

    Когда пользователь нажимал на что-то на экране, в презентер приходило событие и он создавал новую модель, которую отрисовывал специальный класс:


    class SomeViewStateResolver(private val stateMachine: SomeViewStateMachine) :
           Consumer<SomeViewState> {
    
       override fun accept(stateUpdate: SomeViewState) {
           if (stateUpdate.result != null) {
               stateMachine.toSuccess(stateUpdate.result)
           } else if (stateUpdate.error != null && stateUpdate.progress == false) {
               stateMachine.toError(stateUpdate.errorMessage)
           } else if (stateUpdate.progress == true) {
               stateMachine.toProgress()
           } else if (stateUpdate.someButtonEnabled != null) {
               stateMachine.changeSomeButton(stateUpdate.someButtonEnabled)
           }
       }
    }

    Согласитесь, какой-то странный MVP, да и от MVI далеко. Ищем вдохновение.


    Глава 2. Redux



    Общаясь о своих проблемах с другими разработчиками, наш (тогда еще) лид Сергей Боиштян узнал про Redux.


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


    Action
    Описывает действие.


    ActionCreator
    Он как системный аналитик: форматирует, дополняет ТЗ заказчика так, чтобы его понимали программисты.
    Когда пользователь кликает на экран, ActionsCreator формирует Action, который идет в middleware (какая-то бизнес-логика). Бизнес-логика отдает нам новые данные, которые получает и отрисовывает определенный Reducer.


    Если вы еще раз посмотрите на картинку, то можете заметить такой объект, как Store. Store хранит в себе Reducer’ы. То есть мы видим, что фронтендеры — братья по несчастью — догадались, что можно один большой объект распилить на много маленьких, каждый из которых будет отвечать за свою часть экрана. И это просто замечательная мысль!


    Примеры кода простых ActionCreator’ов (осторожно, JavaScript!):


    export function addTodo(text) {
      return { type: ADD_TODO, text }
    }
    export function toggleTodo(index) {
      return { type: TOGGLE_TODO, index }
    }
    export function setVisibilityFilter(filter) {
      return { type: SET_VISIBILITY_FILTER, filter }
    }

    Reducer


    Actions описывает факт, что что-то произошло, но не указывает, как состояние приложения должно измениться в ответ, это работа для Reducer'а.

    Короче говоря, Reducer знает, как декомпозированно обновлять экран/view.


    Плюсы:


    • Декомпозированное обновление экрана.
    • Однонаправленный поток данных.

    Минусы:


    • Снова любимый switch.
      function todoApp(state = initialState, action) {
      switch (action.type) {
      case SET_VISIBILITY_FILTER:
        return Object.assign({}, state, {
          visibilityFilter: action.filter
        })
      case ADD_TODO:
        return Object.assign({}, state, {
          todos: [
            ...state.todos,
            {
              text: action.text,
              completed: false
            }
          ]
        })
      default:
        return state
      }
    • Куча объектов состояния.
    • Разделение логики на ActionCreator и Reducer.

    Да, нам показалось, что разделение на ActionCreator и Reducer не самый удачный вариант связки модели и экрана, потому что писать instanceof(is) — плохой подход. И тут-то мы и изобрели НАШУ архитектуру!


    Глава 3. EBA



    Что такое Action и ActionCreator в контексте EBA:


    typealias Action = () -> Unit
    typealias ActionMapper<T> = (T) -> Action
    interface ActionCreator<T> : (T) -> (Observable<Action>)

    Да, половина архитектуры — это typealias’ы и интерфейс. Простота равно изящность!


    Action нужен для того, чтобы что-то вызвать без передачи каких-то данных. Так как ActionCreator возвращает Observable, нам пришлось обернуть Action в еще одну лямбду для передачи каких-то данных. Так и получился ActionMapper — типизированный Action, через который мы можем передавать то, что нам нужно для обновления экрана/view.

    Основные постулаты:

    Один ActionCreator — одна часть экрана

    С первым пунктом все ясно: чтобы не было ада из непонятных перекрестных обновлений, мы договорились, что один ActionCreator может обновлять только свою часть экрана. Если это список — он обновляет только список, если кнопка — только ее.


    Dagger не нужен

    Но, спрашивается, чем нам Dagger не угодил? Рассказываю.
    Типичная история, когда на проекте есть абстрактный Сергей aka даггер-мастер aka «А что эта аннотация делает?».


    Получается так, что, если ты экспериментировал с даггером, приходится объяснять каждый раз каждому новому (да и не только новому) разработчику. А может, ты и сам уже забыл, что эта аннотация делает, и идешь гуглить.


    Все это сильно усложняет процесс создания фичи, не привнося особого удобства. Поэтому мы решили, что будем создавать необходимые нам вещи руками, так это будет быстрее собираться, ведь нет никакой кодогенерации. Да, мы потратим лишних пять минут на написание руками всех зависимостей, но сэкономим много времени на компиляции. Да, мы не везде отказались от даггера, он используется на глобальном уровне, создает какие-то общие вещи, но и их — для большей оптимизации — мы пишем на Java, чтобы не привлекать kapt.


    Схема архитектуры:



    Component это аналог того самого компонента из Dagger’a, только без Dagger’a. Его задача — создать Binder. Binder связывает воедино ActionCreator’ы. Из View в Binder приходят Events о том, что произошло, а из Binder’a во View отправляются Actions, которые обновляют экран.


    ActionCreator



    Теперь давайте разберемся, что это за штука такая — ActionCreator. В самом простом случае он просто однонаправленно обрабатывает действие. Допустим, есть такой сценарий: пользователь кликнул на кнопку «Создать задачу». Должен открыться другой экран, где мы будем ее описывать, без всяких дополнительных запросов.


    Для этого мы просто подписываемся на кнопку с помощью RxBinding от нашего любимого Джейка и ждем, когда пользователь на нее кликнет. Как только произойдет клик — Binder отправит Event в конкретный ActionCreator, который вызовет наш Action, который откроет нам новый экран. Заметьте, тут не было никаких switch. Дальше я покажу в коде, почему именно так.
    Если нам вдруг надо сходить в сеть или в базу, мы делаем эти запросы тут же, но через интеракторы, которые мы передали в конструктор ActionCreator’a по интерфейсу их вызова:


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

    class LoadItemsActionCreator(
              private val getItems: () -> Observable<List<ViewTyped>>,
              private val showLoadedItems: ActionMapper<DiffResult<ViewTyped>>,
              private val diffCalculator: DiffCalculator<ViewTyped>,
              private val errorItem: ErrorView,
              private val emptyItem: ViewTyped? = null) : ActionOnEvent

    Под словами «по интерфейсу их вызова» я имел в виду как раз то, как объявляется getItems (здесь ViewTyped — это наш интерфейс для работы со списками). Кстати, этот ActionCreator у нас переиспользуется в восьми разных частях приложения, потому что он написан максимально универсально.


    Так как события имеют реактивную природу, мы можем собирать цепочку, добавляя туда другие операторы, например startWith(showLoadingAction), чтобы показать загрузку, и onErrorReturn(errorAction), чтобы показать стейт экрана с ошибкой.
    И все это реактивно!


    Пример


    class AboutFragment : CompositionFragment(R.layout.fragment_about) {
      override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
       super.onViewCreated(view, savedInstanceState)
         val component = AboutComponent(
                    setVersionName = { { appVersion.text = it } },
                    openPdfAction = { (url, name) -> { openPdf(url, name) } })
         val events = AboutEventsImpl(
                 bindEvent = bindEvent,
                 openPolicyPrivacyEvent = confidentialityPolicy.clicks(),
                 openProcessingPersDataEvent = personalDataProtection.clicks(),
                 unbindEvent = unBindEvent)
    
         component.binder().bind(events)
       }

    Давайте уже наконец рассмотрим архитектуру на примере кода. Для начала я выбрал один из самых простых экранов — о приложении, потому что это статичный экран.
    Рассмотрим создание компонента:


    val component = AboutComponent(
            setVersionName = { { appVersion.text = it } },
            openPdfAction = { (url, name) -> { openPdf(url, name) } }
                                  )

    Аргументы компонента — Action’ы/ActionMapper’ы — помогают связать View с ActionCreator’ами. В ActionMapper’e setVersionName мы передаем версию проекта и присваиваем тексту на экране это значение. В openPdfAction — пару из ссылки на документ и имени для открытия следующего экрана, где пользователь может прочитать этот документ.


    Вот так выглядит сам компонент:


    class AboutComponent(
                     private val setVersionName: ActionMapper<String>,
                     private val openPdfAction: ActionMapper<Pair<String, String>>) {
    
       fun binder(): AboutEventsBinder {
           val openPolicyPrivacy = OpenPdfActionCreator(openPdfAction, someUrlString)
           val openProcessingPersonalData = OpenPdfActionCreator(openPdfAction, 
                                                                 anotherUrlString)
           val setVersionName = setVersionName.toSimpleActionCreator(
                                                    moreComponent::currentVersionName
                                                                  )
           return AboutEventsBinder(setVersionName, 
                                    openPolicyPrivacy, 
                                    openProcessingPersonalData)
       }
    }

    Напомню, что:


    typealias Action = () -> Unit
    typealias ActionMapper<T> = (T) -> Action

    Окей, идем дальше.


    fun binder(): AboutEventsBinder

    Давайте рассмотрим AboutEventsBinder подробнее.


    class AboutEventsBinder(private val setVersionName: ActionOnEvent,
                            private val openPolicyPrivacy: ActionOnEvent,
                            private val openProcessingPersonalData: ActionOnEvent) :
           BaseEventsBinder<AboutEvents>() {
    
       override fun bindInternal(events: AboutEvents): Observable<Action> {
           return Observable.merge(
             setVersionName(events.bindEvent),
             openPolicyPrivacy(events.openPolicyPrivacyEvent),
             openProcessingPersonalData(events.openProcessingPersDataEvent))
       }
    }

    ActionOnEvent — это очередной typealias, чтобы не писать каждый раз.


    ActionCreator<Observable<*>>

    В AboutEventsBinder мы передаем ActionCreator’ы и, вызывая их, связываем с конкретным событием. Но, чтобы понять, как всё это связывается, давайте рассмотрим базовый класс — BaseEventsBinder.


    abstract class BaseEventsBinder<in EVENTS : BaseEvents>(
       private val uiScheduler: Scheduler = AndroidSchedulers.mainThread()
                                                           ) {
    
       fun bind(events: EVENTS) {
           bindInternal(events).observeOn(uiScheduler)
                   .takeUntil(events.unbindEvent)
                   .subscribe(Action::invoke)
       }
    
       protected abstract fun bindInternal(events: EVENTS): Observable<Action>
    }
    

    Видим знакомый метод bindInternal, который мы переопределили в наследнике. Теперь рассмотрим метод bind. Вся магия заключена тут. Мы принимаем наследника интерфейса BaseEvents, передаем его в bindInternal для связи Events и Actions. Один раз говорим, что всё, что бы ни пришло, исполняем на ui-потоке и подписываемся. Также видим интересный хак — takeUntil.


    interface BaseEvents {
       val unbindEvent: EventObservable
    }

    Определив в BaseEvents поле unbindEvent для контроля отписки, мы обязаны реализовывать ее во всех наследниках. Это замечательное поле позволяет отписываться от цепочки автоматически, как только данный эвент выполнится. Это же просто великолепно! Теперь можно не следить и не париться по поводу жизненного цикла и спать спокойно.


    val openPolicyPrivacy = OpenPdfActionCreator(openPdfAction, policyPrivacyUrl)
    val openProcessingPersonalData = OpenPdfActionCreator(openPdfAction, 
                                                          personalDataUrl)

    Вернемся к компоненту. И уже тут виден способ переиспользования. Мы написали один класс, который умеет открывать экран просмотра pdf, и нам без разницы — с каким url. Никакой дубликации кода.


    class OpenPdfActionCreator(
                       private val openPdfAction: ActionMapper<Pair<String, String>>, 
                       private val pdfUrl: String) : ActionOnEvent {
    
       override fun invoke(event: EventObservable): Observable<Action> {
           return event.map {
               openPdfAction(pdfUrl to pdfUrl.substringAfterLast(FILE_NAME_DELIMITER))
           }
       }
    }

    Код ActionCreator’а тоже максимально простой, тут мы просто производим некоторые манипуляции со строкой.


    Снова вернемся к компоненту и рассмотрим следующий ActionCreator:


    setVersionName.toSimpleActionCreator(moreComponent::currentVersionName)

    Однажды нам стало лень писать одинаковые и простые по своей сути ActionCreator’ы. Мы воспользовались мощью котлина и написали extension’ы. Например, в этом случае нам нужно было просто передать в ActionMapper статическую строку.


    fun <R> ActionMapper<R>.toSimpleActionCreator(
                                      mapper: () -> R): ActionCreator<Observable<*>> {
       return object : ActionCreator<Observable<*>> {
           override fun invoke(event: Observable<*>): Observable<Action> {
               return event.map { this@toSimpleActionCreator(mapper()) }
           }
       }
    }

    Бывают случаи, когда нам вообще ничего передавать не надо, а только вызвать какой-то Action — например, чтобы открыть следующий экран:


    fun Action.toActionCreator(): ActionOnEvent {
       return object : ActionOnEvent {
           override fun invoke(event: EventObservable): Observable<Action> {
               return event.map { this@toActionCreator }
           }
       }
    }

    Итак, с компонентом покончено, возвращаемся во фрагмент:


    val events = AboutEventsImpl(
      bindEvent = bindEvent,
      openPolicyPrivacyEvent = confidentialityPolicy.throttleFirstClicks(),
      openProcessingPersDataEvent = personalDataProtection.throttleFirstClicks(),
      unbindEvent = unBindEvent)

    Здесь мы видим создание класса, отвечающего за прием событий от пользователя. А unbind и bind — это просто события жизненного цикла экрана, которые мы забираем с помощью библиотеки Navi от Trello.


    fun <T> NaviComponent.observe(event: Event<T>): Observable<T> = 
                                                        RxNavi.observe(this, event)
    
    val unBindEvent: Observable<*> = observe(Event.DESTROY_VIEW)
    val bindEvent: Observable<*> = Observable.just(true)
    или
    val bindEvent = observe(Event.POST_CREATE)

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


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


    interface AboutEvents : BaseEvents {
       val bindEvent: EventObservable
       val openPolicyPrivacyEvent: EventObservable
       val openProcessingPersDataEvent: EventObservable
    }
    
    class AboutEventsImpl(override val bindEvent: EventObservable,
                         override val openPolicyPrivacyEvent: EventObservable,
                         override val openProcessingPersDataEvent: EventObservable,
                         override val unbindEvent: EventObservable) : AboutEvents

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


    component.binder().bind(events)

    На этой архитектуре мы пишем проект уже около двух лет. И нет предела счастью менеджеров в скорости деливеринга фич! Они не успевают новую придумать, как мы уже заканчиваем старую. Архитектура очень гибкая и позволяет переиспользовать много кода.
    Минусом данной архитектуры можно назвать несохранение состояния. У нас нет целой модели, описывающей состояние экрана, как в MVI, но мы с этим справляемся. Как — смотрите ниже.


    Глава 4. Бонус


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


    Итак, какая у меня была идея: аналитика обычно уходит в ответ на действия пользователя. А у нас как раз есть класс, который аккумулирует действия пользователя. Окей, приступим.


    Шаг 1. Немного меняем базовый класс BaseEventsBinder, обернув events в trackAnalytics:


    abstract class BaseEventsBinder<in EVENTS : BaseEvents>(
       private val trackAnalytics: TrackAnalytics<EVENTS> = EmptyAnalyticsTracker(),
       private val uiScheduler: Scheduler = AndroidSchedulers.mainThread()) {
    
       @SuppressLint("CheckResult")
       fun bind(events: EVENTS) {
           bindInternal(trackAnalytics(events)).observeOn(uiScheduler)
                   .takeUntil(events.unbindEvent)
                   .subscribe(Action::invoke)
       }
    
       protected abstract fun bindInternal(events: EVENTS): Observable<Action>
    }
    

    Шаг 2. Создаем стабовую реализацию переменной trackAnalytics, чтобы поддержать обратную совместимость и не сломать наследников, которым пока не нужна аналитика:


    interface TrackAnalytics<EVENTS : BaseEvents> {
       operator fun invoke(events: EVENTS): EVENTS
    }
    
    class EmptyAnalyticsTracker<EVENTS : BaseEvents> : TrackAnalytics<EVENTS> {
       override fun invoke(events: EVENTS): EVENTS = events
    }

    Шаг 3. Пишем реализацию интерфейса TrackAnalytics для нужного экрана — допустим, для экрана списка проектов:


    class TrackProjectsEvents : TrackAnalytics<ProjectsEvents> {
       override fun invoke(events: ProjectsEvents): ProjectsEvents {
           return object : ProjectsEvents by events {
               override val boardClickEvent = events.boardClickEvent.trackTypedEvent {
                   allProjectsProjectClick(it.title)
               }
               override val openBoardCreationEvent = 
                                            events.openBoardCreationEvent.trackEvent {
                   allProjectsAddProjectClick()
               }
               override val openCardsSearchEvent = 
                                              events.openCardsSearchEvent.trackEvent {
                   allProjectsSearchBarClick()
               }
           }
       }
    }

    Тут мы опять используем мощь котлина в виде делегатов. У нас уже есть созданный нами наследник интерфейса — в данном случае ProjectsEvents. Но для некоторых событий надо переопределить то, как идут события, и добавить вокруг них обвязку с отсылкой аналитики. На деле trackEvent это просто doOnNext:


    inline fun <T> Observable<T>.trackEvent(crossinline event: AnalyticsSpec.() -> Unit): Observable<T> =
           doOnNext { event(analyticsSpec) }
    
    inline fun <T> Observable<T>.trackTypedEvent(crossinline event: AnalyticsSpec.(T) -> Unit): Observable<T> =
           doOnNext { event(analyticsSpec, it) }

    Шаг 4. Осталось передать это в Binder. Так как мы его конструируем в компоненте, у нас есть возможность, если вдруг понадобится, докинуть в конструктор дополнительные зависимости. Теперь конструктор ProjectsEventsBinder будет выглядеть так:


    class ProjectsEventsBinder(
                            private val loadItems: LoadItemsActionCreator,
                            private val refreshBoards: ActionOnEvent,
                            private val openBoard: ActionCreator<Observable<BoardId>>,
                            private val openScreen: ActionOnEvent,
                            private val openCardSearch: ActionOnEvent,
                            trackAnalytics: TrackAnalytics<ProjectsEvents>) :
           BaseEventsBinder<ProjectsEvents>(trackAnalytics)

    Другие примеры можете посмотреть на GitHub .


    Вопросы и ответы


    Как вы храните стейт экрана?

    Никак. Мы блочим ориентацию. Но также используем arguments/intent и сохраняем туда переменную OPENED_FROM_BACKSTACK. И при конструировании Binder’а смотрим на нее. Если она false — грузим данные из сети. Если true — из кэша. Это позволяет быстро пересоздать экран.


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


    Я не хочу писать компоненты, как мне задружить это все с даггером?

    Не советую, но если не жалко времени на компиляцию — можно создавать Component и через даггер. Но мы не пробовали.


    Я не пишу на котлин, какие сложности с реализацией на Java?

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


    Если вам понравится статья — следующая часть будет про то, как на такой архитектуре писать тесты (тут-то и станет ясно, зачем столько интерфейсов). Спойлер — писать легко и можно писать на все слои, кроме компонента, но его и не нужно тестировать, он просто создает объект binder’а.


    Спасибо коллегам из команды мобильной разработки Тинькофф Бизнес за помощь в составлении статьи.

    Tinkoff.ru
    356,31
    IT’s Tinkoff.ru — просто о сложном
    Поделиться публикацией

    Похожие публикации

    Комментарии 13

      +1
      Скажите сразу — как вам пришло в голову такое название? Это, конечно, не Dynamic PSP, но ведь без тупых шутеек не обошлось?
        +1
        да я сидел, думал, как назвать, жонглировал основными частями архитектуры и как-то буквы сложились в такое хайповое название. Мне показалось оно с одной стороны забавное, с другой стороны реально описывающее действительность. Поэтому решил оставить
        +1
        Для меня как фронтэндера всё это выглядит как «надо же, до кого-то дошло, что можно не писать switch(action.type), а создавать инстансы действий, которые сами знают, что им нужно делать». Что, вообще-то говоря, лежит на поверхности, когда у вас нет установки «делаем всё строго так, как завещал великий редакс».
          0
          Можно немного подробнее, про идею создания инстансов действий, которые сами знают, что им нужно делать
          0
          Минутка занудства — на первой картинке у вас на ленте транспортёра были квадраты, а теперь кругляши, не самая лучшая идея.
            0
            Если вы еще раз посмотрите на картинку, то можете заметить такой объект, как Store. Store хранит в себе Reducer’ы.

            Store не хранит редьюсеры — это объект с несколькими основными функциями: dispatch, getState, subscribe, через которые осуществляется взаимодействие с состоянием и оповещение о его изменении. Редьюсеры — функции, которые определяют правила обработки веток стейта.

            Короче говоря, Reducer знает, как декомпозированно обновлять экран/view.


            Редьюсер вообще ничего не знает про то, как обновлять экран/view. Более того — сам Redux об этом ничего не знает. Это библиотека, предоставляющая инструменты для работы со стейтом и она не завязана ни с чем.
              0

              Я пытался объяснить по-простому и упрощая некоторые вещи. Твои замечания верны, но такое описание сложно понять, если ты не фронтендер со стажем. В нашем, андроидном, мире всё немного по другому. Поэтому я и написал так, чтобы понятно было тем, кто никогда не видел и не слышал что такое Redux


              Редьюсеры — функции, которые определяют правила обработки веток стейта

              Там в начале, перед первой главой, есть эпиграф, который говорит, стейт задаёт то, что видит пользователь. Поэтому, когда я говорю, что редьюсер знает как обновить экран, я имею в виду, что он знает как обновить стейт, от которого зависит экран

                +1
                «Проблематика» перед первой главой написана правильно, согласен. Однако, нижеследующие упрощения будут крайне вредны для новичков. Согласитесь, что «обновление экрана» и «обновление состояния» — не одно и тоже, и даже не аналогия.

                Ведь чтобы обновить компоненты страницы, необходимо связать данные и представление. Это не задача redux. Если view-слой приложения написан на React, то связывание скорее всего будет осуществляться через hoc-коннектор react-redux (+ опционально reselect), для Vue — Vuex и т.д.

                Я надеюсь мы поняли друг друга ;)
              0
              Интересно!
                0

                Выглядит интересно. Только не понял, где у вас стейт хранится? Допустим хочу на новый экран передать текущий выделенный элемент, где он хранится в рамках фичи?

                  0

                  По стейту вопрос я предусмотрел и ответил в конце статьи в "Вопросы и ответы".
                  Не очень понял как стейт связан с передачей объекта на следующий экран. Передать объект можно любым способом удобным вам: через интент/аргументы, через синглтон, через компонент даггера, который живёт для этих двух экранов, через базу данных и какой-то ключ экрана. Варианты зависят от потребностей и размера передаваемых данных.

                    0
                    Я не имел в виду сохранение стейта в onSaveInstanceState. Например вы из сети сущность загрузили и работаете с ней в рамках экрана. Где она хранится у вас? В обычном MVI есть объект State, в MVVM это может быть какая то lateinit проперти.
                      0

                      а, понял.
                      У нас на проекте данных хранятся в объекте, который закрыт интерфейсом(LocalDataSource), за которым может быть как работа с базой, так и мапа в памяти просто. Допустим мы открыли список проектов. Нам пришла модель от бэка, где есть всё, что нам нужно для отображения. Мы её как-то обработали и передали в ScreenNameLocalDataSource. Дальше подписались на изменения этих данных в ActionCreator через соответсвующий интерактор и обновляем экран на основании этих данных.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое