Преобразование потоков данных

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

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

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

Примените базовые преобразования

Поскольку PagingData инкапсулируется в реактивный поток, вы можете применять операции преобразования к данным постепенно между загрузкой данных и их представлением.

Чтобы применить преобразования к каждому объекту PagingData в потоке, поместите преобразования внутри операции map() в потоке:

Котлин

pager.flow // Type is Flow<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map { pagingData ->
    // Transformations in this block are applied to the items
    // in the paged data.
}

Ява

PagingRx.getFlowable(pager) // Type is Flowable<PagingData<User>>.
  // Map the outer stream so that the transformations are applied to
  // each new generation of PagingData.
  .map(pagingData -> {
    // Transformations in this block are applied to the items
    // in the paged data.
  });

Ява

// Map the outer stream so that the transformations are applied to
// each new generation of PagingData.
Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData -> {
    // Transformations in this block are applied to the items
    // in the paged data.
  });

Преобразование данных

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

Одним из распространенных вариантов использования этого является сопоставление объекта уровня сети или базы данных с объектом, специально используемым на уровне пользовательского интерфейса. В примере ниже показано, как применить этот тип операции с картой:

Котлин

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.map { user -> UiModel(user) }
  }

Ява

// Type is Flowable<PagingData<User>>.
PagingRx.getFlowable(pager)
  .map(pagingData ->
    pagingData.map(UiModel.UserModel::new)
  )

Ява

Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData ->
    pagingData.map(UiModel.UserModel::new)
)

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

Вы можете прослушивать ввод запроса с помощью потокового API. Сохраните ссылку на поток в вашей ViewModel . Уровень пользовательского интерфейса не должен иметь к нему прямого доступа; вместо этого определите функцию для уведомления ViewModel о запросе пользователя.

Котлин

private val queryFlow = MutableStateFlow("")

fun onQueryChanged(query: String) {
  queryFlow.value = query
}

Ява

private BehaviorSubject<String> querySubject = BehaviorSubject.create("");

public void onQueryChanged(String query) {
  queryFlow.onNext(query)
}

Ява

private MutableLiveData<String> queryLiveData = new MutableLiveData("");

public void onQueryChanged(String query) {
  queryFlow.setValue(query)
}

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

Котлин

val querySearchResults = queryFlow.flatMapLatest { query ->
  // The database query returns a Flow which is output through
  // querySearchResults
  userDatabase.searchBy(query)
}

Ява

Observable<User> querySearchResults =
  querySubject.switchMap(query -> userDatabase.searchBy(query));

Ява

LiveData<User> querySearchResults = Transformations.switchMap(
  queryLiveData,
  query -> userDatabase.searchBy(query)
);

Использование таких операций, как flatMapLatest или switchMap гарантирует, что в пользовательский интерфейс будут возвращены только самые последние результаты. Если пользователь меняет входные данные запроса до завершения операции с базой данных, эти операции отбрасывают результаты старого запроса и немедленно запускают новый поиск.

Фильтровать данные

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

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

Котлин

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
  }

Ява

// Type is Flowable<PagingData<User>>.
PagingRx.getFlowable(pager)
  .map(pagingData ->
    pagingData.filter(user -> !user.isHiddenFromUi())
  )
}

Ява

Transformations.map(
  // Type is LiveData<PagingData<User>>.
  PagingLiveData.getLiveData(pager),
  pagingData ->
    pagingData.filter(user -> !user.isHiddenFromUi())
)

Добавить разделители списка

Библиотека подкачки поддерживает разделители динамических списков. Вы можете улучшить читаемость списка, вставив разделители непосредственно в поток данных в виде элементов списка RecyclerView . В результате разделители представляют собой полнофункциональные объекты ViewHolder , обеспечивающие интерактивность, фокус доступности и все другие функции, предоставляемые View .

Вставка разделителей в постраничный список состоит из трех шагов:

  1. Преобразуйте модель пользовательского интерфейса, чтобы разместить элементы-разделители.
  2. Преобразуйте поток данных, чтобы динамически добавлять разделители между загрузкой и представлением данных.
  3. Обновите пользовательский интерфейс для обработки элементов-разделителей.

Преобразование модели пользовательского интерфейса

Библиотека подкачки вставляет разделители списков в RecyclerView как фактические элементы списка, но элементы-разделители должны отличаться от элементов данных в списке, чтобы они могли привязываться к другому типу ViewHolder с отдельным пользовательским интерфейсом. Решение состоит в том, чтобы создать запечатанный класс Kotlin с подклассами для представления ваших данных и разделителей. Альтернативно вы можете создать базовый класс, который будет расширен за счет класса элемента списка и класса разделителя.

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

Котлин

sealed class UiModel {
  class UserModel(val id: String, val label: String) : UiModel() {
    constructor(user: User) : this(user.id, user.label)
  }

  class SeparatorModel(val description: String) : UiModel()
}

Ява

class UiModel {
  private UiModel() {}

  static class UserModel extends UiModel {
    @NonNull
    private String mId;
    @NonNull
    private String mLabel;

    UserModel(@NonNull String id, @NonNull String label) {
      mId = id;
      mLabel = label;
    }

    UserModel(@NonNull User user) {
      mId = user.id;
      mLabel = user.label;
    }

    @NonNull
    public String getId() {
      return mId;
    }

    @NonNull
    public String getLabel() {
      return mLabel;
      }
    }

    static class SeparatorModel extends UiModel {
    @NonNull
    private String mDescription;

    SeparatorModel(@NonNull String description) {
      mDescription = description;
    }

    @NonNull
    public String getDescription() {
      return mDescription;
    }
  }
}

Ява

class UiModel {
  private UiModel() {}

  static class UserModel extends UiModel {
    @NonNull
    private String mId;
    @NonNull
    private String mLabel;

    UserModel(@NonNull String id, @NonNull String label) {
      mId = id;
      mLabel = label;
    }

    UserModel(@NonNull User user) {
      mId = user.id;
      mLabel = user.label;
    }

    @NonNull
    public String getId() {
      return mId;
    }

    @NonNull
    public String getLabel() {
      return mLabel;
      }
    }

    static class SeparatorModel extends UiModel {
    @NonNull
    private String mDescription;

    SeparatorModel(@NonNull String description) {
      mDescription = description;
    }

    @NonNull
    public String getDescription() {
      return mDescription;
    }
  }
}

Преобразование потока данных

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

  • Преобразуйте загруженные элементы списка, чтобы отразить новый тип базового элемента.
  • Используйте метод PagingData.insertSeparators() чтобы добавить разделители.

Дополнительные сведения об операциях преобразования см. в разделе Применение базовых преобразований .

В следующем примере показаны операции преобразования для обновления потока PagingData<User> в поток PagingData<UiModel> с добавленными разделителями:

Котлин

pager.flow.map { pagingData: PagingData<User> ->
  // Map outer stream, so you can perform transformations on
  // each paging generation.
  pagingData
  .map { user ->
    // Convert items in stream to UiModel.UserModel.
    UiModel.UserModel(user)
  }
  .insertSeparators<UiModel.UserModel, UiModel> { before, after ->
    when {
      before == null -> UiModel.SeparatorModel("HEADER")
      after == null -> UiModel.SeparatorModel("FOOTER")
      shouldSeparate(before, after) -> UiModel.SeparatorModel(
        "BETWEEN ITEMS $before AND $after"
      )
      // Return null to avoid adding a separator between two items.
      else -> null
    }
  }
}

Ява

// Map outer stream, so you can perform transformations on each
// paging generation.
PagingRx.getFlowable(pager).map(pagingData -> {
  // First convert items in stream to UiModel.UserModel.
  PagingData<UiModel> uiModelPagingData = pagingData.map(
    UiModel.UserModel::new);

  // Insert UiModel.SeparatorModel, which produces PagingData of
  // generic type UiModel.
  return PagingData.insertSeparators(uiModelPagingData,
    (@Nullable UiModel before, @Nullable UiModel after) -> {
      if (before == null) {
        return new UiModel.SeparatorModel("HEADER");
      } else if (after == null) {
        return new UiModel.SeparatorModel("FOOTER");
      } else if (shouldSeparate(before, after)) {
        return new UiModel.SeparatorModel("BETWEEN ITEMS "
          + before.toString() + " AND " + after.toString());
      } else {
        // Return null to avoid adding a separator between two
        // items.
        return null;
      }
    });
});

Ява

// Map outer stream, so you can perform transformations on each
// paging generation.
Transformations.map(PagingLiveData.getLiveData(pager),
  pagingData -> {
    // First convert items in stream to UiModel.UserModel.
    PagingData<UiModel> uiModelPagingData = pagingData.map(
      UiModel.UserModel::new);

    // Insert UiModel.SeparatorModel, which produces PagingData of
    // generic type UiModel.
    return PagingData.insertSeparators(uiModelPagingData,
      (@Nullable UiModel before, @Nullable UiModel after) -> {
        if (before == null) {
          return new UiModel.SeparatorModel("HEADER");
        } else if (after == null) {
          return new UiModel.SeparatorModel("FOOTER");
        } else if (shouldSeparate(before, after)) {
          return new UiModel.SeparatorModel("BETWEEN ITEMS "
            + before.toString() + " AND " + after.toString());
        } else {
          // Return null to avoid adding a separator between two
          // items.
          return null;
        }
      });
  });

Обработка разделителей в пользовательском интерфейсе

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

Вы также должны внести следующие изменения в адаптер списка:

  • Добавьте регистры в методы onCreateViewHolder() и onBindViewHolder() для учета элементов списка разделителей.
  • Реализуйте новый компаратор.

Котлин

class UiModelAdapter :
  PagingDataAdapter<UiModel, RecyclerView.ViewHolder>(UiModelComparator) {

  override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
  ) = when (viewType) {
    R.layout.item -> UserModelViewHolder(parent)
    else -> SeparatorModelViewHolder(parent)
  }

  override fun getItemViewType(position: Int) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    return when (peek(position)) {
      is UiModel.UserModel -> R.layout.item
      is UiModel.SeparatorModel -> R.layout.separator_item
      null -> throw IllegalStateException("Unknown view")
    }
  }

  override fun onBindViewHolder(
    holder: RecyclerView.ViewHolder,
    position: Int
  ) {
    val item = getItem(position)
    if (holder is UserModelViewHolder) {
      holder.bind(item as UserModel)
    } else if (holder is SeparatorModelViewHolder) {
      holder.bind(item as SeparatorModel)
    }
  }
}

object UiModelComparator : DiffUtil.ItemCallback<UiModel>() {
  override fun areItemsTheSame(
    oldItem: UiModel,
    newItem: UiModel
  ): Boolean {
    val isSameRepoItem = oldItem is UiModel.UserModel
      && newItem is UiModel.UserModel
      && oldItem.id == newItem.id

    val isSameSeparatorItem = oldItem is UiModel.SeparatorModel
      && newItem is UiModel.SeparatorModel
      && oldItem.description == newItem.description

    return isSameRepoItem || isSameSeparatorItem
  }

  override fun areContentsTheSame(
    oldItem: UiModel,
    newItem: UiModel
  ) = oldItem == newItem
}

Ява

class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> {
  UiModelAdapter() {
    super(new UiModelComparator(), Dispatchers.getMain(),
      Dispatchers.getDefault());
  }

  @NonNull
  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    int viewType) {
    if (viewType == R.layout.item) {
      return new UserModelViewHolder(parent);
    } else {
      return new SeparatorModelViewHolder(parent);
    }
  }

  @Override
  public int getItemViewType(int position) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    UiModel item = peek(position);
    if (item instanceof UiModel.UserModel) {
      return R.layout.item;
    } else if (item instanceof UiModel.SeparatorModel) {
      return R.layout.separator_item;
    } else {
      throw new IllegalStateException("Unknown view");
    }
  }

  @Override
  public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,
    int position) {
    if (holder instanceOf UserModelViewHolder) {
      UserModel userModel = (UserModel) getItem(position);
      ((UserModelViewHolder) holder).bind(userModel);
    } else {
      SeparatorModel separatorModel = (SeparatorModel) getItem(position);
      ((SeparatorModelViewHolder) holder).bind(separatorModel);
    }
  }
}

class UiModelComparator extends DiffUtil.ItemCallback<UiModel> {
  @Override
  public boolean areItemsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    boolean isSameRepoItem = oldItem instanceof UserModel
      && newItem instanceof UserModel
      && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId());

    boolean isSameSeparatorItem = oldItem instanceof SeparatorModel
      && newItem instanceof SeparatorModel
      && ((SeparatorModel) oldItem).getDescription().equals(
      ((SeparatorModel) newItem).getDescription());

    return isSameRepoItem || isSameSeparatorItem;
  }

  @Override
  public boolean areContentsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    return oldItem.equals(newItem);
  }
}

Ява

class UiModelAdapter extends PagingDataAdapter<UiModel, RecyclerView.ViewHolder> {
  UiModelAdapter() {
    super(new UiModelComparator(), Dispatchers.getMain(),
      Dispatchers.getDefault());
  }

  @NonNull
  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
    int viewType) {
    if (viewType == R.layout.item) {
      return new UserModelViewHolder(parent);
    } else {
      return new SeparatorModelViewHolder(parent);
    }
  }

  @Override
  public int getItemViewType(int position) {
    // Use peek over getItem to avoid triggering page fetch / drops, since
    // recycling views is not indicative of the user's current scroll position.
    UiModel item = peek(position);
    if (item instanceof UiModel.UserModel) {
      return R.layout.item;
    } else if (item instanceof UiModel.SeparatorModel) {
      return R.layout.separator_item;
    } else {
      throw new IllegalStateException("Unknown view");
    }
  }

  @Override
  public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder,
    int position) {
    if (holder instanceOf UserModelViewHolder) {
      UserModel userModel = (UserModel) getItem(position);
      ((UserModelViewHolder) holder).bind(userModel);
    } else {
      SeparatorModel separatorModel = (SeparatorModel) getItem(position);
      ((SeparatorModelViewHolder) holder).bind(separatorModel);
    }
  }
}

class UiModelComparator extends DiffUtil.ItemCallback<UiModel> {
  @Override
  public boolean areItemsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    boolean isSameRepoItem = oldItem instanceof UserModel
      && newItem instanceof UserModel
      && ((UserModel) oldItem).getId().equals(((UserModel) newItem).getId());

    boolean isSameSeparatorItem = oldItem instanceof SeparatorModel
      && newItem instanceof SeparatorModel
      && ((SeparatorModel) oldItem).getDescription().equals(
      ((SeparatorModel) newItem).getDescription());

    return isSameRepoItem || isSameSeparatorItem;
  }

  @Override
  public boolean areContentsTheSame(@NonNull UiModel oldItem,
    @NonNull UiModel newItem) {
    return oldItem.equals(newItem);
  }
}

Избегайте дублирования работы

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

Операция cachedIn() кэширует результаты любых преобразований, произошедших до нее. Следовательно, cachedIn() должен быть последним вызовом в вашей ViewModel.

Котлин

pager.flow // Type is Flow<PagingData<User>>.
  .map { pagingData ->
    pagingData.filter { user -> !user.hiddenFromUi }
      .map { user -> UiModel.UserModel(user) }
  }
  .cachedIn(viewModelScope)

Ява

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
PagingRx.cachedIn(
  // Type is Flowable<PagingData<User>>.
  PagingRx.getFlowable(pager)
    .map(pagingData -> pagingData
      .filter(user -> !user.isHiddenFromUi())
      .map(UiModel.UserModel::new)),
  viewModelScope);
}

Ява

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
PagingLiveData.cachedIn(
  Transformations.map(
    // Type is LiveData<PagingData<User>>.
    PagingLiveData.getLiveData(pager),
    pagingData -> pagingData
      .filter(user -> !user.isHiddenFromUi())
      .map(UiModel.UserModel::new)),
  viewModelScope);

Дополнительные сведения об использовании cachedIn() с потоком PagingData см. в разделе Настройка потока PagingData .

Дополнительные ресурсы

Чтобы узнать больше о библиотеке подкачки, см. следующие дополнительные ресурсы:

Кодлабы

Образцы

{% дословно %} {% дословно %} {% дословно %} {% дословно %}