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

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

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

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

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

Поскольку 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.
}

Преобразовать данные

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

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

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

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

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

private val queryFlow = MutableStateFlow("")

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

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

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

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

Фильтрация данных

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

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

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

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

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

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

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

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

Библиотека Paging вставляет разделители списка в пользовательский интерфейс как фактические элементы списка, но элементы-разделители должны быть различимы от элементов данных в списке, чтобы гарантировать, что оба составных типа отображаются отдельно. Решение состоит в создании закрытого класса 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()
}

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

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

  • Преобразуйте загруженные элементы списка в соответствии с новым базовым типом элемента.
  • Для добавления разделителей используйте метод 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
    }
  }
}

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

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

@Composable fun UserList(pagingItems: LazyPagingItems) {
  LazyColumn {
    items(
      count = pagingItems.itemCount,
      key = { index ->
        val item = pagingItems.peek(index)
        when (item) {
          is UiModel.UserModel -> item.user.id
          is UiModel.SeparatorModel -> item.description
          else -> index
        }
      }
    ) { index ->
      when (val item = pagingItems[index]) {
        is UiModel.UserModel -> UserItemComposable(item.user)
        is UiModel.SeparatorModel -> SeparatorComposable(item.description)
        null -> PlaceholderComposable()
      }
    }
  }
}

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

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

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

Для корректного управления кэшем передайте CoroutineScope в cachedIn() , как показано в следующем примере с использованием viewModelScope .

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

Для получения дополнительной информации об использовании cachedIn() с потоком PagingData см. раздел «Настройка потока PagingData» .

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

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

Документация

Просмотры контента

{% verbatim %} {% endverbatim %} {% verbatim %} {% endverbatim %}