بارگذاری و نمایش داده‌های صفحه‌بندی‌شده (Views)

مفاهیم و پیاده‌سازی Jetpack Compose

کتابخانه Paging قابلیت‌های قدرتمندی را برای بارگذاری و نمایش داده‌های صفحه‌بندی‌شده از یک مجموعه داده بزرگتر فراهم می‌کند. این راهنما نحوه استفاده از کتابخانه Paging را برای راه‌اندازی جریانی از داده‌های صفحه‌بندی‌شده از یک منبع داده شبکه و نمایش آن در یک RecyclerView نشان می‌دهد.

تعریف منبع داده

اولین قدم، تعریف یک پیاده‌سازی PagingSource برای شناسایی منبع داده است. کلاس PagingSource API شامل متد load است که شما آن را برای نشان دادن نحوه بازیابی داده‌های صفحه‌بندی شده از منبع داده مربوطه، بازنویسی می‌کنید.

برای استفاده از کوروتین‌های کاتلین جهت بارگذاری ناهمگام، مستقیماً از کلاس PagingSource استفاده کنید. کتابخانه Paging همچنین کلاس‌هایی را برای پشتیبانی از سایر فریم‌ورک‌های ناهمگام ارائه می‌دهد:

  • برای استفاده از RxJava، به جای آن RxPagingSource را پیاده‌سازی کنید.
  • برای استفاده از ListenableFuture از Guava، به جای آن ListenableFuturePagingSource را پیاده‌سازی کنید.

انتخاب انواع کلید و مقدار

PagingSource<Key, Value> دو پارامتر نوع دارد: Key و Value . کلید، شناسه‌ای را که برای بارگذاری داده‌ها استفاده می‌شود تعریف می‌کند و مقدار، نوع خود داده‌ها است. برای مثال، اگر صفحات اشیاء User را با ارسال شماره صفحات Int به Retrofit از شبکه بارگذاری می‌کنید، Int به عنوان نوع Key و User به عنوان نوع Value انتخاب کنید.

تعریف منبع صفحه‌بندی

مثال زیر یک PagingSource پیاده‌سازی می‌کند که صفحات آیتم‌ها را بر اساس شماره صفحه بارگذاری می‌کند. نوع Key برابر با Int و نوع Value User است.

جاوا (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;
  }
}

جاوا (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;
  }
}

یک پیاده‌سازی معمول PagingSource پارامترهای ارائه شده در سازنده‌اش را به متد load ارسال می‌کند تا داده‌های مناسب برای یک پرس‌وجو را بارگذاری کند. در مثال بالا، این پارامترها عبارتند از:

  • backend : نمونه‌ای از سرویس backend که داده‌ها را فراهم می‌کند
  • query : عبارت جستجویی که قرار است به سرویس مشخص شده توسط backend ارسال شود.

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

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

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

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

در هر فراخوانی بارگذاری، ExamplePagingSource کلید فعلی را دریافت کرده و کلید بعدی را برای بارگذاری برمی‌گرداند.
شکل ۱. نموداری که نحوه‌ی استفاده و به‌روزرسانی کلید load را نشان می‌دهد.

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

مدیریت خطاها

درخواست‌های بارگذاری داده‌ها می‌توانند به دلایل مختلفی با شکست مواجه شوند، به خصوص هنگام بارگذاری از طریق شبکه. خطاهای رخ داده در هنگام بارگذاری را با بازگرداندن شیء LoadResult.Error از متد load گزارش دهید.

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

جاوا (RxJava)

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

جاوا (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، به نمونه‌های موجود در مرجع PagingSource API مراجعه کنید.

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

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

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

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

جاوا (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);

جاوا (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 جریان داده را قابل اشتراک‌گذاری می‌کند و داده‌های بارگذاری شده را با CoroutineScope ارائه شده، ذخیره می‌کند. این مثال از viewModelScope ارائه شده توسط lifecycle-viewmodel-ktx artifact استفاده می‌کند.

شیء Pager متد load را از شیء PagingSource فراخوانی می‌کند، شیء LoadParams را در اختیار آن قرار می‌دهد و در عوض شیء LoadResult دریافت می‌کند.

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

همچنین باید یک آداپتور (adapter) برای دریافت داده‌ها در لیست 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)
  }
}

جاوا (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);
  }
}

جاوا (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);
  }
}

آداپتور شما همچنین باید متدهای 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
  }
}

جاوا (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);
  }
}

جاوا (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);
  }
}

داده‌های صفحه‌بندی‌شده را در رابط کاربری خود نمایش دهید

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

مراحل زیر را در متد onCreate مربوط به activity یا متد onViewCreated مربوط به fragment خود انجام دهید:

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

جاوا (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));

جاوا (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 اکنون داده‌های صفحه‌بندی شده را از منبع داده نمایش می‌دهد و در صورت لزوم، به طور خودکار صفحه دیگری را بارگذاری می‌کند.