بارگیری و نمایش داده های صفحه بندی شده

کتابخانه 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 است.

کاتلین

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

جاوا

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

جاوا

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 : نمونه ای از سرویس Backend که داده ها را فراهم می کند
  • query : عبارت جستجو برای ارسال به سرویس نشان داده شده توسط backend

شی LoadParams حاوی اطلاعاتی درباره عملیات بارگذاری است که باید انجام شود. این شامل کلیدی است که باید بارگذاری شود و تعداد مواردی که باید بارگذاری شوند.

شی LoadResult حاوی نتیجه عملیات بارگذاری است. LoadResult یک کلاس مهر و موم شده است که بسته به موفقیت آمیز بودن فراخوانی load() یکی از دو شکل را دارد:

  • اگر بارگیری موفقیت آمیز بود، یک شی LoadResult.Page را برگردانید.
  • اگر بارگیری موفقیت آمیز نبود، یک شی LoadResult.Error را برگردانید.

شکل زیر نشان می دهد که چگونه تابع load() در این مثال کلید را برای هر بار دریافت می کند و کلید بار بعدی را ارائه می دهد.

در هر فراخوانی load() ExamplePagingSource کلید جاری را دریافت می کند و کلید بعدی را برای بارگیری برمی گرداند.
شکل 1. نمودار نشان می دهد که چگونه load() از کلید استفاده می کند و به روز می کند.

پیاده سازی PagingSource باید یک متد getRefreshKey() را نیز پیاده سازی کند که یک شی PagingState را به عنوان پارامتر می گیرد. هنگامی که داده ها پس از بارگذاری اولیه رفرش یا باطل می شوند، کلید را برای عبور به load() برمی گرداند. کتابخانه صفحه‌بندی این روش را به‌طور خودکار در بازخوانی‌های بعدی داده‌ها فراخوانی می‌کند.

رسیدگی به خطاها

درخواست‌ها برای بارگیری داده‌ها ممکن است به دلایلی با شکست مواجه شوند، به‌ویژه هنگام بارگیری از طریق شبکه. با برگرداندن یک شی LoadResult.Error از متد load() خطاهایی را که در حین بارگیری با آنها مواجه شده است گزارش کنید.

به عنوان مثال، می توانید با اضافه کردن موارد زیر به متد load() خطاهای بارگذاری را در ExamplePagingSource از مثال قبلی دریافت و گزارش کنید:

کاتلین

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

جاوا

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

جاوا

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 API مراجعه کنید.

PagingSource اشیاء LoadResult.Error را جمع آوری و به UI تحویل می دهد تا بتوانید بر اساس آنها عمل کنید. برای اطلاعات بیشتر در مورد نمایش وضعیت بارگیری در رابط کاربری، به مدیریت و ارائه وضعیت های بارگیری مراجعه کنید.

یک جریان از PagingData را تنظیم کنید

در مرحله بعد، به جریانی از داده های صفحه بندی شده از پیاده سازی PagingSource نیاز دارید. جریان داده را در ViewModel خود تنظیم کنید. کلاس Pager متدهایی را ارائه می دهد که یک جریان واکنشی از اشیاء PagingData را از یک PagingSource نشان می دهد. کتابخانه Paging از چندین نوع جریان، از جمله Flow ، LiveData ، و انواع Flowable و Observable از RxJava پشتیبانی می کند.

هنگامی که یک نمونه Pager برای تنظیم جریان واکنشی خود ایجاد می کنید، باید نمونه را با یک شی پیکربندی PagingConfig و یک تابع ارائه کنید که Pager می گوید چگونه یک نمونه از پیاده سازی PagingSource خود را دریافت کند:

کاتلین

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)

جاوا

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

جاوا

// 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 lifecycle-viewmodel-ktx استفاده می کند.

شی Pager متد load() را از شی PagingSource فراخوانی می کند و شیء LoadParams را به آن ارائه می دهد و در عوض شی LoadResult دریافت می کند.

یک آداپتور RecyclerView را تعریف کنید

همچنین باید یک آداپتور برای دریافت داده ها در لیست RecyclerView خود تنظیم کنید. کتابخانه Paging کلاس PagingDataAdapter را برای این منظور فراهم می کند.

کلاسی را تعریف کنید که PagingDataAdapter را گسترش دهد. در مثال، UserAdapter PagingDataAdapter را گسترش می‌دهد تا یک آداپتور RecyclerView را برای آیتم‌های لیست از نوع User و با استفاده از UserViewHolder به‌عنوان نگهدارنده نمایش ارائه کند:

کاتلین

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

جاوا

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

جاوا

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 انجام می شود:

کاتلین

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

جاوا

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

جاوا

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

داده های صفحه شده را در UI خود نمایش دهید

اکنون که یک PagingSource تعریف کرده‌اید، راهی برای برنامه‌تان ایجاد کرده‌اید تا جریانی از PagingData تولید کند، و یک PagingDataAdapter تعریف کرده‌اید، آماده‌اید تا این عناصر را به یکدیگر متصل کنید و داده‌های صفحه‌شده را در فعالیت خود نمایش دهید.

مراحل زیر را در روش onCreate یا قطعه onViewCreated در فعالیت خود انجام دهید:

  1. یک نمونه از کلاس PagingDataAdapter خود ایجاد کنید.
  2. نمونه PagingDataAdapter را به لیست RecyclerView که می‌خواهید داده‌های صفحه‌شده‌تان را نمایش دهد، ارسال کنید.
  3. جریان PagingData را مشاهده کنید و هر مقدار تولید شده را به متد submitData() آداپتور خود ارسال کنید.

کاتلین

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

جاوا

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

جاوا

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، به منابع اضافی زیر مراجعه کنید:

Codelabs

نمونه ها

{% کلمه به کلمه %} {% آخر کلمه %} {% کلمه به کلمه %} {% آخر کلمه %}