Как загружать и показывать данные со страниц

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

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

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

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

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

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

Определите PagingSource

В следующем примере реализуется PagingSource, который загружает страницы элементов по номеру страницы. Тип Key – Int, а тип Value – User.

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 содержит информацию о выполняемой операции загрузки. В частности, это ключ и количество объектов, которые нужно загрузить. Чтобы различать типы загрузки в load, проверьте, какой подкласс LoadParams был передан: LoadParams.Refresh, LoadParams.Append или LoadParams.Prepend.

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

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

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

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

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

// Only for positional or offset PagingSources where Key is a 0-based item index
// rather than a page number.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
  // Center the refreshed load around the user's current viewport so the list
  // does not jump back to the top on invalidation.
  return state.anchorPosition?.let { anchorPosition ->
    (anchorPosition - state.config.initialLoadSize / 2).coerceAtLeast(0)
  }
}

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

Запросы на загрузку данных могут завершаться ошибкой по ряду причин, особенно при загрузке по сети. Сообщайте об ошибках, возникающих во время загрузки, возвращая объект 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. Вы также можете указать необязательный параметр initialKey, например 1 для нумерации страниц, начинающейся с 1, или 0 для нумерации страниц со смещением. В PagingConfig можно настроить pageSize, initialLoadSize, prefetchDistance и enablePlaceholders. Свойство prefetchDistance определяет, сколько элементов от любого края загруженного контента запускают следующую выборку.

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

    val userPagingFlow: Flow<PagingData<User>> = Pager(
        config = PagingConfig(
            pageSize = 20,
            // Load 3 pages initially so the user can scroll immediately without
            // hitting a loading state.
            initialLoadSize = 60,
            // Fetch the next page far enough ahead of the scroll position to
            // avoid visible pauses.
            prefetchDistance = 20,
            enablePlaceholders = true
        ),
        initialKey = 1,
        pagingSourceFactory = {
            ExamplePagingSource(backend, query)
        }
    )
    .flow
    .cachedIn(viewModelScope)
}

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

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

Сбор и показ данных в интерфейсе

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

@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 в блоке контента.

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

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

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

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

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