تحميل البيانات المقسّمة على صفحات وعرضها

توفر مكتبة تسجيل الصفحات إمكانات قوية لتحميل وعرض البيانات المقسّمة إلى صفحات من مجموعة بيانات أكبر. يوضِّح هذا الدليل كيفية استخدام مكتبة "تتبُّع الصفحات" لإعداد تدفق من البيانات المقسّمة على صفحات من مصدر بيانات شبكة وعرضها في RecyclerView.

تحديد مصدر بيانات

تتمثّل الخطوة الأولى في تحديد عملية تنفيذ PagingSource لتحديد مصدر البيانات. تتضمّن فئة واجهة برمجة التطبيقات PagingSource الطريقة load() التي تلغيها للإشارة إلى كيفية استرداد البيانات المقسّمة إلى صفحات من مصدر البيانات المقابل.

استخدِم الفئة PagingSource مباشرةً لاستخدام الكوروتينات في لغة Kotlin للتحميل غير المتزامن. توفر مكتبة تسجيل الصفحات أيضًا فئات لدعم الأطر الأخرى غير المتزامنة:

اختيار أنواع المفتاح والقيمة

تتضمّن PagingSource<Key, Value> مَعلمة نوع: Key وValue. يحدد المفتاح المعرف المستخدم لتحميل البيانات، والقيمة هي نوع البيانات نفسها. على سبيل المثال، إذا حمّلت صفحات من كائنات User من الشبكة عن طريق تمرير أرقام Int إلى التعديل، اختَر Int كنوع Key وUser كنوع Value.

تحديد مصدر الترحيل

ينفِّذ المثال التالي سمة PagingSource تحمِّل صفحات من العناصر حسب رقم الصفحة. النوع Key هو Int والنوع Value هو User.

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

تعمل طريقة تنفيذ PagingSource النموذجية على تمرير المعلَمات المتوفرة في أداة الإنشاء إلى طريقة load() لتحميل البيانات المناسبة لطلب بحث. في المثال أعلاه، هذه المعلمات هي:

  • backend: مثيل من خدمة الخلفية التي توفر البيانات
  • query: طلب البحث المطلوب إرساله إلى الخدمة المُشار إليها في backend

يحتوي الكائن LoadParams على معلومات عن عملية التحميل المطلوب تنفيذها. يتضمن ذلك المفتاح المراد تحميله وعدد العناصر المراد تحميلها.

يحتوي الكائن LoadResult على نتيجة عملية التحميل. والفئة LoadResult هي فئة مختومة تتخذ أحد شكلَين لها، استنادًا إلى ما إذا كان استدعاء load() قد نجح أم لا:

  • وفي حال نجاح التحميل، يمكنك عرض العنصر LoadResult.Page.
  • وإذا لم ينجح التحميل، يمكنك عرض عنصر LoadResult.Error.

يوضح الشكل التالي كيف تتلقى الدالة load() في هذا المثال المفتاح لكل تحميل وتوفر المفتاح للتحميل اللاحق.

في كل استدعاء read()، يأخذ ExamplePagingSource المفتاح الحالي ويعرض المفتاح التالي لتحميله.
الشكل 1. رسم بياني يوضّح طريقة استخدام load() للمفتاح وتعديله

يجب أن تنفذ PagingSource أيضًا طريقة getRefreshKey() تستخدِم عنصر PagingState كمَعلمة. وهو يعرض المفتاح لتمريره إلى طريقة load() عند إعادة تحميل البيانات أو إلغائها بعد التحميل الأولي. تستدعي مكتبة الترحيل هذه الطريقة تلقائيًا عند عمليات التحديث التالية للبيانات.

معالجة الأخطاء

قد تفشل طلبات تحميل البيانات لعدد من الأسباب، خاصةً عند التحميل عبر إحدى الشبكات. يمكنك الإبلاغ عن الأخطاء التي حدثت أثناء التحميل من خلال عرض عنصر LoadResult.Error من طريقة load().

على سبيل المثال، يمكنك اكتشاف أخطاء التحميل في ExamplePagingSource والإبلاغ عنها من المثال السابق عن طريق إضافة ما يلي إلى طريقة load():

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

لمزيد من المعلومات عن معالجة أخطاء التعديل، يُرجى الاطّلاع على النماذج في مرجع واجهة برمجة التطبيقات PagingSource.

يجمع PagingSource كائنات LoadResult.Error ويقدمها إلى واجهة المستخدم حتى تتمكن من اتخاذ إجراءات بشأنها. لمزيد من المعلومات حول عرض حالة التحميل في واجهة المستخدم، يمكنك الاطّلاع على إدارة حالات التحميل وعرضها.

إعداد ساحة مشاركات لبيانات التنقل

بعد ذلك، ستحتاج إلى مصدر بيانات مقسّمة على صفحات من تنفيذ PagingSource. عليك إعداد مصدر البيانات في ViewModel. توفّر الفئة Pager طرقًا تعرض بثًا تفاعليًا لعناصر PagingData من PagingSource. تتيح مكتبة تسجيل الصفحات استخدام عدة أنواع من مجموعات البث، منها Flow وLiveData وFlowable وObservable من RxJava.

عند إنشاء مثيل Pager لإعداد البث التفاعلي، يجب تزويد المثيل بكائن ضبط PagingConfig ودالة توضّح طريقة الحصول على مثيل من تنفيذ PagingSource:Pager

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() مصدر البيانات قابلاً للمشاركة ويخزِّن البيانات المحمَّلة مؤقتًا باستخدام CoroutineScope المقدَّمة. يستخدم هذا المثال السمة viewModelScope المتوفّرة من خلال عنصر lifecycle-viewmodel-ktx لدورة الحياة.

يستدعي الكائن Pager الطريقة load() من الكائن PagingSource، ويقدّمه بالكائن LoadParams، ويحصل على الكائن LoadResult في المقابل.

تحديد محوّل RecyclerView

يجب أيضًا إعداد محوّل لاستلام البيانات في قائمة RecyclerView الخاصة بك. وتوفّر "مكتبة تسجيل الصفحات" فئة PagingDataAdapter لهذا الغرض.

حدِّد فئة تمتد إلى PagingDataAdapter. في المثال، يوسّع UserAdapter PagingDataAdapter لتوفير محوِّل RecyclerView لعناصر القائمة من النوع User ويستخدم UserViewHolder كملف شخصي:

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

يجب أن يحدّد المحوِّل أيضًا طريقتَي onCreateViewHolder() وonBindViewHolder() وأن يحدّدا DiffUtil.ItemCallback. ويعمل ذلك بالطريقة نفسها التي تتّبعها عادةً عند تحديد معدِّلات قائمة RecyclerView:

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

عرض البيانات المقسّمة على صفحات في واجهة المستخدم

الآن، بعد أن حددت PagingSource، وأنشأت طريقة لتطبيقك لإنشاء ساحة مشاركات PagingData، وحددت PagingDataAdapter، فأنت جاهز لربط هذه العناصر معًا وعرض البيانات المقسّمة على صفحات في نشاطك.

نفِّذ الخطوات التالية في طريقة onCreate أو طريقة القسم onViewCreated في نشاطك:

  1. أنشئ مثيلاً لفئة PagingDataAdapter.
  2. مرِّر مثيل PagingDataAdapter إلى قائمة RecyclerView التي تريد عرض البيانات المقسّمة على صفحات.
  3. راقِب ساحة مشاركات PagingData ومرِّر كل قيمة تم إنشاؤها إلى طريقة submitData() للمعدِّل.

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 الآن البيانات المقسّمة على صفحات من مصدر البيانات وتحمِّل صفحة أخرى تلقائيًا عند الضرورة.

مراجع إضافية

لمعرفة المزيد من المعلومات حول مكتبة تسجيل الصفحات، اطّلِع على المراجع الإضافية التالية:

الدروس التطبيقية حول الترميز

عيّنات