Загрузка и отображение постраничных данных

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

Определите источник данных

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

Используйте класс PagingSource напрямую, чтобы применять сопрограммы Kotlin для асинхронной загрузки.

Выберите типы ключей и значений.

PagingSource<Key, Value> имеет два параметра типа: Key и Value . Параметр Key определяет идентификатор, используемый для загрузки данных, а параметр Value — тип самих данных. Например, если вы загружаете страницы объектов User из сети, передавая в Retrofit Int номера страниц, выберите Int в качестве типа Key и User в качестве типа Value .

Определите источник пейджинга

В следующем примере реализован компонент PagingSource , который загружает страницы элементов по номеру страницы. Тип KeyInt , а тип ValueUser .

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {

    init {
        // the data source is expected to be immutable
        // invalidate PagingSource if data source
        // has updated
        backEnd.addDatabaseOnChangedListener {
            invalidate()
        }
    }

    try {
      // Start refresh at page 1 if undefined.
      val nextPageNumber = params.key ?: 1
      val response = backend.searchUsers(query, nextPageNumber)
      return LoadResult.Page(
        data = response.users,
        prevKey = null, // Only paging forward.
        nextKey = nextPageNumber + 1
      )
    } catch (e: Exception) {
      // Handle errors in this block and return LoadResult.Error for
      // expected errors (such as a network failure).
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // Try to find the page key of the closest page to anchorPosition from
    // either the prevKey or the nextKey; you need to handle nullability
    // here.
    //  * prevKey == null -> anchorPage is the first page.
    //  * nextKey == null -> anchorPage is the last page.
    //  * both prevKey and nextKey are null -> anchorPage is the
    //    initial page, so return null.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

Типичная реализация PagingSource передает параметры, указанные в конструкторе, методу load для загрузки соответствующих данных для запроса. В приведенном выше примере этими параметрами являются:

  • backend : экземпляр серверной службы, предоставляющей данные.
  • query : поисковый запрос, который необходимо отправить в указанный backend сервис.

Объект LoadParams содержит информацию о выполняемой операции загрузки. Сюда входят загружаемый ключ и количество загружаемых элементов.

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

  • Если загрузка прошла успешно, верните объект LoadResult.Page .
  • Если загрузка не удалась, верните объект LoadResult.Error .
  • Если объект PagingSource больше недействителен и должен быть заменен новым экземпляром (например, из-за изменения базовых данных), верните объект LoadResult.Invalid .

На следующем рисунке показано, как функция load в этом примере получает ключ для каждой загрузки и предоставляет ключ для последующей загрузки.

При каждом вызове функции загрузки объект ExamplePagingSource принимает текущий ключ и возвращает следующий ключ для загрузки.
Рисунок 1. Диаграмма, показывающая, как load используется и обновляется ключ.

Реализация PagingSource также должна реализовывать метод getRefreshKey , который принимает в качестве параметра объект PagingState . Он возвращает ключ, который передается в метод load при обновлении или аннулировании данных после первоначальной загрузки. Библиотека Paging автоматически вызывает этот метод при последующих обновлениях данных.

Обработка ошибок

Запросы на загрузку данных могут завершаться неудачей по ряду причин, особенно при загрузке по сети. Сообщайте об ошибках, возникших во время загрузки, возвращая объект LoadResult.Error из метода load .

Например, вы можете перехватывать и сообщать об ошибках загрузки в ExamplePagingSource из предыдущего примера, добавив следующее в метод load :

catch (e: IOException) {
  // IOException for network failures.
  return LoadResult.Error(e)
} catch (e: HttpException) {
  // HttpException for any non-2xx HTTP status codes.
  return LoadResult.Error(e)
}

Для получения дополнительной информации об обработке ошибок Retrofit см. примеры в справочнике API PagingSource .

PagingSource собирает и передает объекты LoadResult.Error в пользовательский интерфейс, чтобы вы могли с ними взаимодействовать. Дополнительную информацию о отображении состояния загрузки в пользовательском интерфейсе см. в разделе «Управление и отображение состояний загрузки» .

Настройте поток PagingData.

Далее вам потребуется поток постраничных данных из реализации PagingSource . Настройте поток данных в вашем ViewModel . Класс Pager предоставляет методы, которые предоставляют реактивный поток объектов PagingData из PagingSource . Библиотека Paging предоставляет поток данных в виде Flow .

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

class UserViewModel(
    private val backend: ExampleBackendService,
    private val query: String
) : ViewModel() {

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        // Configure how data is loaded by passing additional properties to
        // PagingConfig, such as pageSize and enabling or disabling placeholders.
        config = PagingConfig(
            pageSize = 20,
            enablePlaceholders = true
        ),
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

Оператор cachedIn делает поток данных доступным для совместного использования и кэширует загруженные данные с помощью предоставленного CoroutineScope . Без cachedIn PagingData не могут быть повторно получены. В этом примере используется viewModelScope предоставляемый артефактом lifecycle-viewmodel-ktx .

Объект Pager вызывает метод load из объекта PagingSource , передавая ему объект LoadParams и получая в ответ объект LoadResult .

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

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

@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
    val userFlow = viewModel.userPagingFlow
    UserList(flow = userFlow)
}

Используйте collectAsLazyPagingItems для преобразования потока PagingData в LazyPagingItems . Затем используйте API items внутри LazyColumn для размещения каждого элемента.

Обязательно укажите уникальный и стабильный идентификатор для каждого элемента, используя itemKey . В следующем примере используется it.id (ссылка на свойство User.id ), поскольку он остается стабильным для экземпляра User при обновлении данных.

@Composable
fun UserList(flow: Flow<PagingData<User>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it.id }
        ) { index ->
            val user = lazyPagingItems[index]
            if (user != null) {
                UserRow(user)
            } else {
                UserPlaceholder()
            }
        }
    }
}

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

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

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

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

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

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

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