» » » Реактивные приложения с Model-View-Intent. Часть 3: State Reducer

 

Реактивные приложения с Model-View-Intent. Часть 3: State Reducer

Автор: admin от 12-02-2018, 15:50, посмотрело: 114

Реактивные приложения с Model-View-Intent. Часть 3: State Reducer


В предыдущей части мы обсудили, как реализовать простой экран с паттерном Model-View-Intent, использующим однонаправленный поток данных. В третьей части мы построим более сложный экран с MVI с помощью State Reducer.

github).

Теперь давайте сфокусируемся на модели. Как уже упоминалось в предыдущих частях — модель должна отражать состояние. Так что, представляю вам модель, которая называется HomeViewState.



public final class HomeViewState {

  private final boolean loadingFirstPage;
  private final Throwable firstPageError;
  private final List<FeedItem> data;
  private final boolean loadingNextPage;
  private final Throwable nextPageError;
  private final boolean loadingPullToRefresh;
  private final Throwable pullToRefreshError;

   // ... конструктор ...
   // ... геттеры  ...
}


Обратите внимание на то, что FeedItem — это просто интерфейс, который должен реализовывать каждый элемент, отображаемый в RecyclerView. К примеру, Product реализовывает FeedItem. Также отображаемое название категории SectionHeader реализовывает FeedItem. UI-элемент, который показывает, что могут быть загружены дополнительные элементы категории, является FeedItem и содержит внутри своё состояние для того, чтобы отобразить, загружаем ли мы дополнительные элементы в определенной категории:



public class AdditionalItemsLoadable implements FeedItem {

  private final int moreItemsAvailableCount;
  private final String categoryName;
  private final boolean loading;
  private final Throwable loadingError;

   // ... конструктор ...
   // ... геттеры  ...
}


А так же создадим элемент бизнес-логики HomeFeedLoader, который отвечает за загрузку FeedItems:



public class HomeFeedLoader {
  public Observable<List<FeedItem loadNewestPage() { ... }
  public Observable<List<FeedItem loadFirstPage() { ... }
  public Observable<List<FeedItem loadNextPage() { ... }
  public Observable<List<Product loadProductsOfCategory(String categoryName) { ... }
}


Теперь давайте всё соединим шаг за шагом в нашем Презентере. Имейте в виду, что некоторый код, представленный здесь, как часть Презентера, скорее всего должен быть перенесен в Interactor в реальном приложении (что я не сделал для лучшей читаемости). Во-первых, загрузим начальные данные:



class HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {

  private final HomeFeedLoader feedLoader;

  @Override 
  protected void bindIntents() {
    Observable<HomeViewState> loadFirstPage = intent(HomeView::loadFirstPageIntent)
        .flatMap(ignored  feedLoader.loadFirstPage()
            .map(items  new HomeViewState(items, false, null) )
            .startWith(new HomeViewState(emptyList, true, null) )
            .onerrorReturn(error  new HomeViewState(emptyList, false, error))

    subscribeViewState(loadFirstPage, HomeView::render);
  }
}


Пока всё идет хорошо, нет больших отличий с тем, как мы реализовали “экран поиска” в части 2. Теперь давайте попробуем добавить поддержку Pull-To-Refresh:



class HomePresenter extends MviBasePresenter<HomeView, HomeViewState> {

  private final HomeFeedLoader feedLoader;

  @Override 
  protected void bindIntents() {
    Observable<HomeViewState> loadFirstPage = ... ;

    Observable<HomeViewState> pullToRefresh = intent(HomeView::pullToRefreshIntent)
        .flatMap(ignored  feedLoader.loadNewestPage()
            .map( items  new HomeViewState(...))
            .startWith(new HomeViewState(...))
            .onerrorReturn(error  new HomeViewState(...)));

    Observable<HomeViewState> allIntents = Observable.merge(loadFirstPage, pullToRefresh);

    subscribeViewState(allIntents, HomeView::render);
  }
}


Но постойте: feedLoader.loadNewestPage() возвращает только новые элементы, а что же с предыдущими элементами, которые мы загрузили ранее? В “традиционном” MVP кто-то может сделать view.addNewItems(newItems), но в первой части мы уже обсудили, почему это плохая идея («Проблема состояния») Проблема, с которой мы сейчас столкнулись, такова: Pull-To-Refresh зависит от предыдущего HomeViewState, так как мы хотим объединить предыдущие элементы с элементами, которые вернулись от Pull-To-Refresh.



Дамы и Господа, прошу любить и жаловать — State Reducer



Реактивные приложения с Model-View-Intent. Часть 3: State Reducer



State Reducer является концептом из функционального программирования. Он принимает предыдущее состояние на вход и вычисляет новое состояние из предыдущего состояния:



public State reduce( State previous, Foo foo ){
  State newState;
  // ... вычисление нового State на основании предыдущего с применением Foo
  return newState;
}


Идея в том, что метод reduce() объединяет предыдущее состояние с foo, чтобы вычислить новое состояние. Foo обычно представляет собой изменения, которые мы хотим применить к предыдущему состоянию. В нашем случае, мы хотим объединить предыдущий HomeViewState (изначально полученный от loadFirstPageIntent) с результатами от Pull-To-Refresh. Оказывается, в RxJava есть специальный оператор для этого — scan(). Давайте изменим наш код немного. Нам нужно создать еще один класс, который будет отражать частичное изменение (в коде выше он называется Foo) и использоваться для вычисления нового состояния:





Теперь каждый Intent возвращает Observable вместо Observable. Затем мы объединяем их в один Observable с помощью Observable.merge() и наконец применяем оператор Observable.scan(). Это означает, что когда бы пользователь не запустил intent, этот intent создаст объекты PartialState, которые будут сведены к HomeViewState, который в свою очередь будет отображен на View (HomeView.render(HomeViewState)). Единственная пропущенная часть — это сама функция сведения. Сам по себе класс HomeViewState не изменился, но мы добавили Builder (паттерн Builder) и теперь можем создавать новые объекты HomeViewState удобным способом. Теперь давайте реализуем функцию сведения:





Я знаю, что все эти проверки instanceof не очень хорошие, но смысл этой статьи не в этом. Почему технические блоггеры пишут «плохой» код, как в примере выше? Потому что мы хотим сконцентрироваться на определенной теме без того, чтобы заставлять читателя держать в голове весь исходный код (например, нашего приложения с корзиной товаров) или знать определенные паттерны проектирования. Поэтому я считаю, что лучше избегать в статье паттернов, которые сделают код лучше, но также могут привести к худшей читаемости. В центре внимания этой статьи — State Reducer. Глядя на него с проверками instanceof, любой может понять, что он делает. Должны ли вы использовать проверки instanceof в вашем приложении? Нет, используйте паттерны проектирования или другие решения. К примеру, можно объявить PartialState интерфейсом с методом public HomeViewState computeNewState(previousState). В целом, вы можете найти RxSealedUnions от Paco Estevez полезным, когда будете разрабатывать приложения с MVI.



Окей, я думаю вы поняли идею работы State Reducer. Давайте реализуем оставшийся функционал: пагинацию и возможность загрузить больше элементов определенной категории.





Реализация пагинации (загрузки следующей “страницы” с элементами) довольно похожа на pull-to-refresh за исключением того, что мы добавляем загруженные элементы в конец списка, вместо того, чтобы добавлять их в начало (как мы делаем при pull-to-refresh) Интереснее то, как мы загружаем больше элементов определенной категории. Для отображения индикатора загрузки или кнопки повтора/ошибки для выбранной категории мы просто должны найти соответствующий объект AdditionalItemsLoadable в списке всех FeedItem. Затем мы меняем этот элемент, чтобы отобразить индикатор загрузки или кнопку повтора/ошибку. Если мы успешно загрузили все элементы в определенной категории — мы ищем SectionHeader и AdditionalItemsLoadable, а затем заменяем все элементы между ними новыми загруженными элементами.



Заключение



Целью этой статьи было показать, как State Reducer может помочь нам проектировать сложные экраны с помощью небольшого и понятного кода. Просто сделайте шаг назад и подумайте, как бы вы реализовали это с “традиционным” MVP или MVVM без State Reducer? Ключевым моментом, который позволяет нам использовать State Reducer, является то, что у нас присутствует модель, которая отражает состояние. Поэтому, было очень важно понять из первой части этой серии статей, что такое Модель. Так же, State Reducer может быть использован, только если мы можем быть уверены, что Состояние (а точнее — Модель) исходит из единого источника. Поэтому, однонаправленный поток данных также очень важен. Я думаю, теперь понятно, почему мы остановились на этих темах в первой и второй части этой серии статей, и надеюсь у вас произошел тот самый “ага!” момент, когда все точки соединяются вместе. Если нет — не переживайте, для меня это заняло достаточно много времени ( и много практики, и много ошибок и повторений).

Возможно, вы задаетесь вопросом — почему мы не использовали State Reducer на экране поиска (во второй части). Использование State Reducer в основном имеет смысл, когда мы каким-то образом зависим от предыдущего состояния.



Последнее, но не менее важное, на чем я хочу остановиться — если вы еще не заметили (без погружения в детали) все наши данные — неизменные (мы всегда создаем новый HomeViewState, мы никогда не вызываем setter-метод на любом из объектов). Поэтому, у вас не должно возникнуть проблем с многопоточностью. Пользователь может сделать pull-to-refresh и в то же время загрузить новую страницу и загрузить больше элементов определенной категории, потому что State Reducer в состоянии произвести правильное состояние без зависимости от порядка http-ответов. В дополнение, мы написали наш код с помощью простых функций, без побочных эффектов. Это делает наш код супертестируемым, воспроизводимым, высокопараллелизуемым и простым в обсуждении.



Конечно, State Reducer не был изобретен для MVI. Вы можете найти концепты State Reducer во множестве библиотек, фреймворков и систем на разных языках программирования. State Reducer великолепно встраивается в философию Model-View-Intent с однонаправленным потоком данных и Моделью, отражающей Состояние.



В следующей части мы сфокусируемся на том, как создать переиспользуемые и реактивные UI-компоненты с MVI.

Источник: Хабрахабр

Категория: Операционные системы » Android

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

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *