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

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

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

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

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

  • لاستخدام RxJava، يمكنك تنفيذ RxPagingSource بدلاً من ذلك.
  • لاستخدام ListenableFuture من Guava، يمكنك تنفيذ ListenableFuturePagingSource بدلاً من ذلك.

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

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

تحديد PagingSource

ينفِّذ المثال التالي 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() في هذا المثال للمفتاح لكل عملية تحميل وتقديم المفتاح لعملية التحميل اللاحقة.

في كلّ استدعاء load()‎، يتلقّى 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);

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

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

إعداد مصدر بيانات PagingData

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

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

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

تحديد مهايئ RecyclerView

عليك أيضًا إعداد محوِّل لتلقّي البيانات في قائمة RecyclerView. توفّر مكتبة Paging فئة 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 list:

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

مصادر إضافية

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

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