Концепции и реализация Jetpack Compose
При работе с постраничными данными часто возникает необходимость преобразовывать поток данных в процессе его загрузки. Например, может потребоваться отфильтровать список элементов или преобразовать элементы в другой тип перед отображением в пользовательском интерфейсе. Другой распространенный вариант использования преобразования потока данных — добавление разделителей для списков .
В более общем смысле, применение преобразований непосредственно к потоку данных позволяет разделять конструкции репозитория и конструкции пользовательского интерфейса.
На этой странице предполагается, что вы знакомы с основными функциями библиотеки Paging .
Примените основные преобразования
Поскольку PagingData инкапсулирована в реактивный поток, вы можете применять операции преобразования к данным постепенно, между загрузкой данных и их отображением.
Для применения преобразований к каждому объекту PagingData в потоке, поместите преобразования внутрь операции map() в потоке:
Java
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. });
Java
// 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 .
Один из распространенных вариантов использования — сопоставление объекта сетевого или баз данных уровня с объектом, специально используемым на уровне пользовательского интерфейса. Пример ниже демонстрирует, как применять этот тип операции сопоставления:
Java
// Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData.map(UiModel.UserModel::new) )
Java
Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData.map(UiModel.UserModel::new) )
Ещё один распространённый способ преобразования данных — это получение от пользователя входных данных, например, строки запроса, и преобразование их в выходные данные запроса для отображения. Для этого необходимо отслеживать и перехватывать входные данные запроса от пользователя, выполнять запрос и отправлять результат запроса обратно в пользовательский интерфейс.
Вы можете отслеживать ввод запроса, используя API потока. Храните ссылку на поток в вашей ViewModel . Слой пользовательского интерфейса не должен иметь к нему прямого доступа; вместо этого определите функцию для уведомления ViewModel о запросе пользователя.
Java
private BehaviorSubject<String> querySubject = BehaviorSubject.create(""); public void onQueryChanged(String query) { queryFlow.onNext(query) }
Java
private MutableLiveData<String> queryLiveData = new MutableLiveData(""); public void onQueryChanged(String query) { queryFlow.setValue(query) }
Когда значение запроса изменяется в потоке данных, можно выполнить операции преобразования значения запроса в нужный тип данных и вернуть результат в пользовательский интерфейс. Конкретная функция преобразования зависит от используемого языка и фреймворка, но все они предоставляют схожую функциональность.
Java
Observable<User> querySearchResults = querySubject.switchMap(query -> userDatabase.searchBy(query));
Java
LiveData<User> querySearchResults = Transformations.switchMap( queryLiveData, query -> userDatabase.searchBy(query) );
Использование таких операций, как flatMapLatest или switchMap гарантирует, что в пользовательский интерфейс будут возвращены только самые последние результаты. Если пользователь изменит свой запрос до завершения операции с базой данных, эти операции отбросят результаты старого запроса и немедленно запустят новый поиск.
Фильтрация данных
Ещё одна распространённая операция — фильтрация. Вы можете фильтровать данные на основе критериев, заданных пользователем, или удалять данные из пользовательского интерфейса, если их следует скрыть на основе других критериев.
Эти операции фильтрации необходимо размещать внутри вызова map() поскольку фильтр применяется к объекту PagingData . После фильтрации данных из объекта PagingData новый экземпляр PagingData передается на уровень пользовательского интерфейса для отображения.
Java
// Type is Flowable<PagingData<User>>. PagingRx.getFlowable(pager) .map(pagingData -> pagingData.filter(user -> !user.isHiddenFromUi()) ) }
Java
Transformations.map( // Type is LiveData<PagingData<User>>. PagingLiveData.getLiveData(pager), pagingData -> pagingData.filter(user -> !user.isHiddenFromUi()) )
Добавить разделители списка
Библиотека Paging поддерживает динамические разделители списков. Вы можете улучшить читаемость списка, вставляя разделители непосредственно в поток данных в качестве элементов списка RecyclerView . В результате разделители становятся полнофункциональными объектами ViewHolder , обеспечивающими интерактивность, фокусировку на доступности и все другие функции, предоставляемые View .
Вставка разделителей в постраничный список состоит из трех шагов:
- Преобразуйте модель пользовательского интерфейса для размещения разделительных элементов.
- Преобразуйте поток данных, чтобы динамически добавлять разделители между загрузкой данных и их отображением.
- Обновите пользовательский интерфейс для обработки разделительных элементов.
Преобразовать модель пользовательского интерфейса
Библиотека Paging вставляет разделители списка в RecyclerView как фактические элементы списка, но элементы-разделители должны отличаться от элементов данных в списке, чтобы их можно было привязать к другому типу ViewHolder с другим пользовательским интерфейсом. Решение состоит в создании закрытого класса Kotlin с подклассами для представления ваших данных и разделителей. В качестве альтернативы вы можете создать базовый класс, который расширяется классом элемента списка и классом разделителя.
Предположим, вы хотите добавить разделители к постраничному списку элементов User . Следующий фрагмент кода показывает, как создать базовый класс, экземпляры которого могут быть либо UserModel , либо SeparatorModel :
Java
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; } } }
Java
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> с добавленными разделителями:
Java
// 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; } }); });
Java
// 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()варианты обработки, учитывающие элементы списка-разделители. - Реализуйте новый компаратор.
Java
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); } }
Java
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.
Java
// 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); }
Java
// 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» .
Дополнительные ресурсы
Чтобы узнать больше о библиотеке пейджинга, ознакомьтесь со следующими дополнительными ресурсами:
Кодлабс
{% verbatim %}Рекомендуем вам
- Примечание: текст ссылки отображается, когда JavaScript отключен.
- Загрузка и отображение постраничных данных
- Протестируйте свою реализацию постраничной навигации.
- Управление и отображение состояний загрузки