Sayfalandırılmış verileri (görünümler) yükleme ve görüntüleme

Kavramlar ve Jetpack Compose uygulaması

Paging kitaplığı, daha büyük bir veri kümesinden sayfalandırılmış verileri yükleme ve görüntüleme için güçlü özellikler sunar. Bu kılavuzda, bir ağ veri kaynağından sayfalandırılmış veri akışı oluşturmak ve bunu RecyclerView içinde görüntülemek için Paging 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ı, sayfalandırılmış verilerin ilgili veri kaynağından nasıl alınacağını belirtmek için geçersiz kıldığınız load yöntemini içerir.

Asenkron yükleme için Kotlin eş yordamlarını kullanmak üzere PagingSource sınıfını doğrudan kullanın. Paging kitaplığı, diğer eş zamansız çerçeveleri desteklemek için de sınıflar sağlar:

Anahtar ve değer türlerini seçme

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, User nesnelerinin sayfalarını ağdan Retrofit'e Int sayfa numaraları ileterek yüklüyorsanız Int türü olarak Key, User türü olarak Value seçin.

PagingSource'u tanımlayın

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

Java (RxJava)

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 (Guava/LiveData)

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ı, oluşturucusunda sağlanan parametreleri load yöntemine ileterek bir sorgu için uygun verileri yükler. Yukarıdaki örnekte bu parametreler şunlardır:

  • backend: Verileri sağlayan arka uç hizmetinin bir örneği
  • query: backend ile belirtilen hizmete gönderilecek arama sorgusu

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

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 birini alan kapalı bir sınıftır:

  • Yükleme başarılı olursa LoadResult.Page nesnesi döndürülür.
  • Yükleme başarılı olmazsa 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 anahtarı nasıl sağladığı gösterilmektedir.

ExamplePagingSource, her yükleme çağrısında geçerli anahtarı alır ve yüklenecek sonraki anahtarı döndürür.
Şekil 1. load öğesinin anahtarı nasıl kullandığını ve güncellediğini gösteren diyagram.

PagingSource uygulaması, parametre olarak PagingState nesnesi alan bir getRefreshKey yöntemi de uygulamalıdır. İlk yüklemeden sonra veriler yenilendiğinde veya geçersiz kılındığında load yöntemine iletilecek anahtarı döndürür. Paging Library, bu yöntemi verilerin sonraki yenilemelerinde otomatik olarak çağırır.

Hataları işleme

Veri yükleme istekleri, özellikle ağ üzerinden yükleme yapılırken çeşitli nedenlerle 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, ExamplePagingSource yöntemine aşağıdakileri ekleyerek önceki örnekteki load yükleme hatalarını yakalayıp bildirebilirsiniz:

Java (RxJava)

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

Java (Guava/LiveData)

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ı işleme hakkında daha fazla bilgi için PagingSource API referansındaki örneklere bakın.

PagingSource, LoadResult.Error nesnelerini toplar ve kullanıcı arayüzüne iletir. Böylece, bu nesneler üzerinde işlem yapabilirsiniz. Yükleme durumunu kullanıcı arayüzünde gösterme hakkında daha fazla bilgi için Yükleme durumlarını yönetme ve sunma başlıklı makaleyi inceleyin.

PagingData akışı oluşturma

Ardından, PagingSource uygulamasından alınan, sayfalandırılmış bir veri akışına ihtiyacınız vardır. ViewModel uygulamanızda veri akışını ayarlayın. Pager sınıfı, PagingSource kaynağından PagingData nesnelerinin reaktif akışını gösteren yöntemler sağlar. Paging kitaplığı, RxJava'daki Flow, LiveData, Flowable ve Observable türleri de 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 bir PagingConfig yapılandırma nesnesi ve Pager'ya PagingSource uygulamanızın bir örneğini nasıl alacağını söyleyen bir işlev sağlamanız gerekir:

Java (RxJava)

// 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 (Guava/LiveData)

// 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ü viewModelScope yapıtı tarafından sağlanan lifecycle-viewmodel-ktx kullanılıyor.

Pager nesnesi, PagingSource nesnesinden load yöntemini çağırarak LoadParams nesnesini sağlar ve karşılığında LoadResult nesnesini alır.

RecyclerView adaptörü tanımlama

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

PagingDataAdapter öğesini genişleten bir sınıf tanımlayın. Örnekte, UserAdapter extends PagingDataAdapter, User türündeki liste öğeleri için RecyclerView bağdaştırıcısı sağlamak ve UserViewHolder öğesini görünüm tutucu olarak kullanmak üzere kullanılır:

Kotlin (Eş Yordamlar)

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 (RxJava)

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 (Guava/LiveData)

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

Adaptörünüz ayrıca onCreateViewHolder ve onBindViewHolder yöntemlerini tanımlamalı ve bir DiffUtil.ItemCallback belirtmelidir. Bu, RecyclerView liste bağdaştırıcıları tanımlarken normalde olduğu gibi çalışır:

Kotlin (Eş Yordamlar)

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 (RxJava)

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 (Guava/LiveData)

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

Sayfalandırılmış verileri kullanıcı arayüzünüzde gösterme

Artık bir PagingSource tanımladığınıza, uygulamanızın PagingData akışı oluşturmasını sağlayacak bir yöntem oluşturduğunuza ve bir PagingDataAdapter tanımladığınıza göre bu öğeleri birbirine bağlamaya ve sayfalandırılmış verileri etkinliğinizde göstermeye hazırsınız.

Etkinliğinizin onCreate veya parçanızı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. Sayfalandırılmış verilerinizi görüntülemek istediğiniz PagingDataAdapter örneğini RecyclerView listesine iletin.
  3. PagingData yayınını gözlemleyin ve oluşturulan her değeri bağdaştırıcınızın submitData() yöntemine iletin.

Kotlin (Eş Yordamlar)

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 (RxJava)

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 (Guava/LiveData)

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 sayfalandırılmış veriler gösteriliyor ve gerektiğinde otomatik olarak başka bir sayfa yükleniyor.