Sayfalı verileri yükleme ve görüntüleme

Sayfalandırma kitaplığı, daha büyük bir veri kümesinden sayfalandırılmış verileri yüklemek ve görüntülemek için güçlü özellikler sunar. Bu kılavuzda, bir ağ veri kaynağından sayfalandırılmış veri akışı oluşturmak ve bunu bir RecyclerView içinde görüntülemek için Çağrı kitaplığının nasıl kullanılacağı gösterilmektedir.

Veri kaynağı tanımlama

İlk adım, veri kaynağını belirlemek için bir PagingSource uygulaması tanımlamaktır. PagingSource API sınıfı, sayfa bölümü alınan verilerin ilgili veri kaynağından nasıl alınacağını belirtmek için geçersiz kıldığınız load() yöntemini içerir.

Eş zamansız yükleme için Kotlin eş yordalarını kullanmak üzere doğrudan PagingSource sınıfını kullanın. Sayfalandırma kitaplığı, diğer eşzamansız çerçeveleri destekleyecek sınıflar da sağlar:

Anahtar ve değer türlerini seçin

PagingSource<Key, Value> iki tür parametreye sahiptir: Key ve Value. Anahtar, verileri yüklemek için kullanılan tanımlayıcıyı tanımlar. Değer ise verilerin türüdür. Örneğin, Int sayfa numarasını Retrofit'e ileterek ağdan User nesnelerinin sayfalarını yüklerseniz Key türü olarak Int ve Value türü olarak User'i seçin.

Sayfa Kaynağını Tanımlama

Aşağıdaki örnekte, öğe sayfalarını sayfa numarasına göre yükleyen bir PagingSource uygulaması uygulanmaktadır. Key türü Int, Value türü ise User şeklindedir.

Kotlin

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {
    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 = response.nextPageNumber
      )
    } 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)
    }
  }
}

Java

class ExamplePagingSource extends RxPagingSource<Integer, User> {
  @NonNull
  private ExampleBackendService mBackend;
  @NonNull
  private String mQuery;

  ExamplePagingSource(@NonNull ExampleBackendService backend,
    @NonNull String query) {
    mBackend = backend;
    mQuery = query;
  }

  @NotNull
  @Override
  public Single<LoadResult<Integer, User>> loadSingle(
    @NotNull LoadParams<Integer> params) {
    // Start refresh at page 1 if undefined.
    Integer nextPageNumber = params.getKey();
    if (nextPageNumber == null) {
      nextPageNumber = 1;
    }

    return mBackend.searchUsers(mQuery, nextPageNumber)
      .subscribeOn(Schedulers.io())
      .map(this::toLoadResult)
      .onErrorReturn(LoadResult.Error::new);
  }

  private LoadResult<Integer, User> toLoadResult(
    @NonNull SearchUserResponse response) {
    return new LoadResult.Page<>(
      response.getUsers(),
      null, // Only paging forward.
      response.getNextPageNumber(),
      LoadResult.Page.COUNT_UNDEFINED,
      LoadResult.Page.COUNT_UNDEFINED);
  }

  @Nullable
  @Override
  public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) {
    // 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.
    Integer anchorPosition = state.getAnchorPosition();
    if (anchorPosition == null) {
      return null;
    }

    LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
    if (anchorPage == null) {
      return null;
    }

    Integer prevKey = anchorPage.getPrevKey();
    if (prevKey != null) {
      return prevKey + 1;
    }

    Integer nextKey = anchorPage.getNextKey();
    if (nextKey != null) {
      return nextKey - 1;
    }

    return null;
  }
}

Java

class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> {
  @NonNull
  private ExampleBackendService mBackend;
  @NonNull
  private String mQuery;
  @NonNull
  private Executor mBgExecutor;

  ExamplePagingSource(
    @NonNull ExampleBackendService backend,
    @NonNull String query, @NonNull Executor bgExecutor) {
    mBackend = backend;
    mQuery = query;
    mBgExecutor = bgExecutor;
  }

  @NotNull
  @Override
  public ListenableFuture<LoadResult<Integer, User>> loadFuture(@NotNull LoadParams<Integer> params) {
    // Start refresh at page 1 if undefined.
    Integer nextPageNumber = params.getKey();
    if (nextPageNumber == null) {
      nextPageNumber = 1;
    }

    ListenableFuture<LoadResult<Integer, User>> pageFuture =
      Futures.transform(mBackend.searchUsers(mQuery, nextPageNumber),
      this::toLoadResult, mBgExecutor);

    ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture =
      Futures.catching(pageFuture, HttpException.class,
      LoadResult.Error::new, mBgExecutor);

    return Futures.catching(partialLoadResultFuture,
      IOException.class, LoadResult.Error::new, mBgExecutor);
  }

  private LoadResult<Integer, User> toLoadResult(@NonNull SearchUserResponse response) {
    return new LoadResult.Page<>(response.getUsers(),
    null, // Only paging forward.
    response.getNextPageNumber(),
    LoadResult.Page.COUNT_UNDEFINED,
    LoadResult.Page.COUNT_UNDEFINED);
  }

  @Nullable
  @Override
  public Integer getRefreshKey(@NotNull PagingState<Integer, User> state) {
    // 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.
    Integer anchorPosition = state.getAnchorPosition();
    if (anchorPosition == null) {
      return null;
    }

    LoadResult.Page<Integer, User> anchorPage = state.closestPageToPosition(anchorPosition);
    if (anchorPage == null) {
      return null;
    }

    Integer prevKey = anchorPage.getPrevKey();
    if (prevKey != null) {
      return prevKey + 1;
    }

    Integer nextKey = anchorPage.getNextKey();
    if (nextKey != null) {
      return nextKey - 1;
    }

    return null;
  }
}

Tipik bir PagingSource uygulaması, bir sorgu için uygun verileri yüklemek amacıyla oluşturucuda sağlanan parametreleri load() yöntemine iletir. Yukarıdaki örnekte bu parametreler şunlardır:

  • backend: verileri sağlayan arka uç hizmetinin bir örneği
  • query: backend tarafından belirtilen hizmete gönderilecek arama sorgusu

LoadParams nesnesi, gerçekleştirilecek yükleme işlemiyle ilgili bilgileri içerir. Bu, yüklenecek anahtarı ve yüklenecek öğe sayısını içerir.

LoadResult nesnesi, yükleme işleminin sonucunu içerir. LoadResult, load() çağrısının başarılı olup olmamasına bağlı olarak iki biçimden birinde olan kapalı bir sınıftır:

  • Yükleme başarılı olursa LoadResult.Page nesnesini döndürün.
  • Yükleme başarılı olmazsa bir LoadResult.Error nesnesi döndürün.

Aşağıdaki şekilde, bu örnekteki load() işlevinin her yükleme için anahtarı nasıl aldığı ve sonraki yükleme için nasıl anahtar sağlandığı gösterilmektedir.

Her bir load() çağrısında, ExamplePagingSource geçerli anahtarı alır ve yüklenecek sonraki anahtarı döndürür.
Şekil 1. load() ürününün anahtarı nasıl kullandığını ve güncellediğini gösteren şema.

PagingSource uygulaması, bir PagingState nesnesini parametre olarak alan getRefreshKey() yöntemi de uygulamalıdır. İlk yüklemeden sonra veriler yenilendiğinde veya geçersiz kılındığında load() yöntemine geçirilecek anahtarı döndürür. Çağrı Kitaplığı, veriler daha sonra yenilendiğinde bu yöntemi otomatik olarak çağırır.

Hataları işleme

Veri yükleme istekleri çeşitli nedenlerle, özellikle de ağ üzerinden yüklenirken başarısız olabilir. load() yönteminden bir LoadResult.Error nesnesi döndürerek yükleme sırasında karşılaşılan hataları bildirin.

Örneğin, önceki örnekte verilen ExamplePagingSource öğesindeki yükleme hatalarını yakalayıp bildirmek için aşağıdaki yöntemi load() yöntemine ekleyebilirsiniz:

Kotlin

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)
}

Java

return backend.searchUsers(searchTerm, nextPageNumber)
  .subscribeOn(Schedulers.io())
  .map(this::toLoadResult)
  .onErrorReturn(LoadResult.Error::new);

Java

ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform(
  backend.searchUsers(query, nextPageNumber), this::toLoadResult,
  bgExecutor);

ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching(
  pageFuture, HttpException.class, LoadResult.Error::new,
  bgExecutor);

return Futures.catching(partialLoadResultFuture,
  IOException.class, LoadResult.Error::new, bgExecutor);

Retrofit hatalarının ele alınması hakkında daha fazla bilgi için PagingSource API referansındaki örneklere bakın.

PagingSource, işlem yapabilmeniz için LoadResult.Error nesneleri toplayıp kullanıcı arayüzüne gönderir. Kullanıcı arayüzünde yükleme durumunu gösterme hakkında daha fazla bilgi için Yükleme durumlarını yönetme ve sunma bölümüne bakın.

PagingData akışı oluşturun

Ardından, PagingSource uygulamasından sayfalık veri akışı sağlamanız gerekir. ViewModel cihazınızda veri akışını ayarlayın. Pager sınıfı, bir PagingSource'dan PagingData nesnelerinin reaktif bir akışını açığa çıkaran yöntemler sunar. Çağrı kitaplığı, RxJava'daki Flow, LiveData ile Flowable ve Observable türleri dahil olmak üzere çeşitli akış türlerinin kullanılmasını destekler.

Reaktif akışınızı ayarlamak için bir Pager örneği oluşturduğunuzda, örneğe PagingConfig yapılandırma nesnesi ve Pager uygulamanızın bir örneğini nasıl alacağını açıklayan bir işlev sağlamanız gerekir:PagingSource

Kotlin

val flow = Pager(
  // Configure how data is loaded by passing additional properties to
  // PagingConfig, such as prefetchDistance.
  PagingConfig(pageSize = 20)
) {
  ExamplePagingSource(backend, query)
}.flow
  .cachedIn(viewModelScope)

Java

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
  new PagingConfig(/* pageSize = */ 20),
  () -> ExamplePagingSource(backend, query));

Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager);
PagingRx.cachedIn(flowable, viewModelScope);

Java

// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact.
CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel);
Pager<Integer, User> pager = Pager<>(
  new PagingConfig(/* pageSize = */ 20),
  () -> ExamplePagingSource(backend, query));

PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);

cachedIn() operatörü, veri akışını paylaşılabilir hale getirir ve yüklenen verileri, sağlanan CoroutineScope ile önbelleğe alır. Bu örnekte, yaşam döngüsü lifecycle-viewmodel-ktx yapısı tarafından sağlanan viewModelScope kullanılmıştır.

Pager nesnesi, PagingSource nesnesinden load() yöntemini çağırarak bu nesneye LoadParams nesnesini sağlar ve buna karşılık LoadResult nesnesini alır.

RecyclerView adaptörü tanımlama

Ayrıca, verileri RecyclerView listenize almak için bir bağdaştırıcı kurmanız gerekir. Çağrı kitaplığı, bu amaç için PagingDataAdapter sınıfını sağlar.

PagingDataAdapter aralığını genişleten bir sınıf tanımlayın. Örnekte UserAdapter, PagingDataAdapter öğesinin kapsamını User türündeki liste öğeleri için bir RecyclerView bağdaştırıcısı sağlayacak ve UserViewHolder öğesini görünüm sahibi olarak kullanacak şekilde genişletir:

Kotlin

class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) :
  PagingDataAdapter<User, UserViewHolder>(diffCallback) {
  override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
  ): UserViewHolder {
    return UserViewHolder(parent)
  }

  override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
    val item = getItem(position)
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item)
  }
}

Java

class UserAdapter extends PagingDataAdapter<User, UserViewHolder> {
  UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) {
    super(diffCallback);
  }

  @NonNull
  @Override
  public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new UserViewHolder(parent);
  }

  @Override
  public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
    User item = getItem(position);
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item);
  }
}

Java

class UserAdapter extends PagingDataAdapter<User, UserViewHolder> {
  UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) {
    super(diffCallback);
  }

  @NonNull
  @Override
  public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    return new UserViewHolder(parent);
  }

  @Override
  public void onBindViewHolder(@NonNull UserViewHolder holder, int position) {
    User item = getItem(position);
    // Note that item can be null. ViewHolder must support binding a
    // null item as a placeholder.
    holder.bind(item);
  }
}

Bağdaştırıcınız da onCreateViewHolder() ve onBindViewHolder() yöntemlerini tanımlamalı ve bir DiffUtil.ItemCallback belirtmelidir. Bu işlem, RecyclerView listesi adaptörlerini tanımlarken normalde olduğu gibi çalışır:

Kotlin

object UserComparator : DiffUtil.ItemCallback<User>() {
  override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
    // Id is unique.
    return oldItem.id == newItem.id
  }

  override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
    return oldItem == newItem
  }
}

Java

class UserComparator extends DiffUtil.ItemCallback<User> {
  @Override
  public boolean areItemsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    // Id is unique.
    return oldItem.id.equals(newItem.id);
  }

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

Java

class UserComparator extends DiffUtil.ItemCallback<User> {
  @Override
  public boolean areItemsTheSame(@NonNull User oldItem,
    @NonNull User newItem) {
    // Id is unique.
    return oldItem.id.equals(newItem.id);
  }

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

Sayfalara ayrılmış verileri kullanıcı arayüzünüzde gösterme

Bir PagingSource tanımladığınıza, uygulamanızın PagingData akışı oluşturması için bir yol oluşturduğunuza ve bir PagingDataAdapter tanımladığınıza göre, bu öğeleri birbirine bağlamaya ve etkinliğinizde sayfalı veriler göstermeye hazırsınız.

Etkinliğinizin onCreate veya parçasının onViewCreated yönteminde aşağıdaki adımları uygulayın:

  1. PagingDataAdapter sınıfınızın bir örneğini oluşturun.
  2. PagingDataAdapter örneğini, sayfa bölümü verilerinizi görüntülemek istediğiniz RecyclerView listesine iletin.
  3. PagingData akışını gözlemleyin ve oluşturulan her değeri bağdaştırıcınızın submitData() yöntemine iletin.

Kotlin

val viewModel by viewModels<ExampleViewModel>()

val pagingAdapter = UserAdapter(UserComparator)
val recyclerView = findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.adapter = pagingAdapter

// Activities can use lifecycleScope directly; fragments use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  viewModel.flow.collectLatest { pagingData ->
    pagingAdapter.submitData(pagingData)
  }
}

Java

ExampleViewModel viewModel = new ViewModelProvider(this)
  .get(ExampleViewModel.class);

UserAdapter pagingAdapter = new UserAdapter(new UserComparator());
RecyclerView recyclerView = findViewById<RecyclerView>(
  R.id.recycler_view);
recyclerView.adapter = pagingAdapter

viewModel.flowable
  // Using AutoDispose to handle subscription lifecycle.
  // See: https://github.com/uber/AutoDispose.
  .to(autoDisposable(AndroidLifecycleScopeProvider.from(this)))
  .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));

Java

ExampleViewModel viewModel = new ViewModelProvider(this)
  .get(ExampleViewModel.class);

UserAdapter pagingAdapter = new UserAdapter(new UserComparator());
RecyclerView recyclerView = findViewById<RecyclerView>(
  R.id.recycler_view);
recyclerView.adapter = pagingAdapter

// Activities can use getLifecycle() directly; fragments use
// getViewLifecycleOwner().getLifecycle().
viewModel.liveData.observe(this, pagingData ->
  pagingAdapter.submitData(getLifecycle(), pagingData));

RecyclerView listesinde artık veri kaynağındaki sayfa bölümü içeren veriler görüntülenir ve gerektiğinde otomatik olarak başka bir sayfa yüklenir.

Ek kaynaklar

Sayfalama kitaplığı hakkında daha fazla bilgi edinmek için aşağıdaki ek kaynaklara bakın:

Codelab uygulamaları

Sana Özel