إدارة حالات التحميل وتقديمها

تتتبّع مكتبة تسجيل الصفحات حالة طلبات تحميل البيانات المقسّمة على صفحات وتعرضها من خلال الفئة LoadState. يمكن لتطبيقك تسجيل مستمع من خلال PagingDataAdapter لتلقّي معلومات حول الحالة الحالية وتعديل واجهة المستخدم وفقًا لذلك. يتم توفير هذه الحالات من المحوِّل لأنها متزامنة مع واجهة المستخدم. هذا يعني أنّ المستمع هو يتلقّى تحديثات عند تحميل الصفحة على واجهة المستخدم.

يتم توفير إشارة LoadState منفصلة لكل LoadType ونوع مصدر البيانات (إما PagingSource أو RemoteMediator). ويوفِّر العنصر CombinedLoadStates الذي يقدّمه المستمع معلومات عن حالة التحميل من كل هذه الإشارات. ويمكنك استخدام هذه المعلومات المفصّلة لعرض مؤشرات التحميل المناسبة للمستخدمين.

حالات التحميل

تعرض "مكتبة تسجيل الصفحات" حالة التحميل للاستخدام في واجهة المستخدم من خلال الكائن LoadState. يتخذ كائنات LoadState أحد الأشكال الثلاثة بناءً على حالة التحميل الحالية:

  • في حال عدم توفُّر عملية تحميل نشطة وبدون حدوث خطأ، يكون LoadState عبارة عن كائن LoadState.NotLoading. وتتضمّن هذه الفئة الفرعية أيضًا السمة endOfPaginationReached التي تشير إلى ما إذا كان قد تم الوصول إلى نهاية عملية تقسيم النتائج على عدّة صفحات.
  • في حال توفُّر عملية تحميل نشطة، يكون LoadState عبارة عن كائن LoadState.Loading.
  • وفي حال حدوث خطأ، يكون LoadState هو الكائن LoadState.Error.

هناك طريقتان لاستخدام LoadState في واجهة المستخدم: استخدام أداة معالجة، أو استخدام محوِّل قائمة خاص لعرض حالة التحميل مباشرةً في قائمة RecyclerView.

الوصول إلى حالة التحميل من خلال أداة معالجة

للحصول على حالة التحميل للاستخدام العام في واجهة المستخدم، استخدِم ساحة المشاركات loadStateFlow أو طريقة addLoadStateListener() التي توفّرها "PagingDataAdapter". توفّر هذه الآليات إمكانية الوصول إلى كائن CombinedLoadStates الذي يتضمّن معلومات حول سلوك LoadState لكل نوع تحميل.

في المثال التالي، يعرض PagingDataAdapter مكوّنات مختلفة لواجهة المستخدم بناءً على الحالة الحالية لتحميل التحديث:

Kotlin

// Activities can use lifecycleScope directly, but Fragments should instead use
// viewLifecycleOwner.lifecycleScope.
lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    progressBar.isVisible = loadStates.refresh is LoadState.Loading
    retry.isVisible = loadState.refresh !is LoadState.Loading
    errorMsg.isVisible = loadState.refresh is LoadState.Error
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.VISIBLE : View.GONE);
  retry.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.GONE : View.VISIBLE);
  errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error
    ? View.VISIBLE : View.GONE);
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.VISIBLE : View.GONE);
  retry.setVisibility(loadStates.refresh instanceof LoadState.Loading
    ? View.GONE : View.VISIBLE);
  errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error
    ? View.VISIBLE : View.GONE);
});

لمزيد من المعلومات حول CombinedLoadStates، يُرجى الاطّلاع على الوصول إلى معلومات حالة التحميل الإضافية.

عرض حالة التحميل باستخدام محوّل

توفّر "مكتبة تسجيل الصفحات" محوّل قائمة آخر يسمّى LoadStateAdapter بهدف عرض حالة التحميل مباشرةً في القائمة المعروضة للبيانات المقسّمة على صفحات. يوفر هذا المحوّل إمكانية الوصول إلى حالة التحميل الحالية للقائمة، والتي يمكنك تمريرها إلى حامل عرض مخصص يعرض المعلومات.

أولاً، أنشئ فئة لحامل الملفات الشخصية تحتفظ بمراجع التحميل وطرق عرض الأخطاء على شاشتك. أنشئ دالة bind() تقبل LoadState كمَعلمة. من المفترض أن تعمل هذه الدالة على تبديل مستوى رؤية العرض استنادًا إلى معلمة حالة التحميل:

Kotlin

class LoadStateViewHolder(
  parent: ViewGroup,
  retry: () -> Unit
) : RecyclerView.ViewHolder(
  LayoutInflater.from(parent.context)
    .inflate(R.layout.load_state_item, parent, false)
) {
  private val binding = LoadStateItemBinding.bind(itemView)
  private val progressBar: ProgressBar = binding.progressBar
  private val errorMsg: TextView = binding.errorMsg
  private val retry: Button = binding.retryButton
    .also {
      it.setOnClickListener { retry() }
    }

  fun bind(loadState: LoadState) {
    if (loadState is LoadState.Error) {
      errorMsg.text = loadState.error.localizedMessage
    }

    progressBar.isVisible = loadState is LoadState.Loading
    retry.isVisible = loadState is LoadState.Error
    errorMsg.isVisible = loadState is LoadState.Error
  }
}

Java

class LoadStateViewHolder extends RecyclerView.ViewHolder {
  private ProgressBar mProgressBar;
  private TextView mErrorMsg;
  private Button mRetry;

  LoadStateViewHolder(
    @NonNull ViewGroup parent,
    @NonNull View.OnClickListener retryCallback) {
    super(LayoutInflater.from(parent.getContext())
      .inflate(R.layout.load_state_item, parent, false));

    LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView);
    mProgressBar = binding.progressBar;
    mErrorMsg = binding.errorMsg;
    mRetry = binding.retryButton;
  }

  public void bind(LoadState loadState) {
    if (loadState instanceof LoadState.Error) {
      LoadState.Error loadStateError = (LoadState.Error) loadState;
      mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
    }
    mProgressBar.setVisibility(loadState instanceof LoadState.Loading
      ? View.VISIBLE : View.GONE);
    mRetry.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
    mErrorMsg.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
  }
}

Java

class LoadStateViewHolder extends RecyclerView.ViewHolder {
  private ProgressBar mProgressBar;
  private TextView mErrorMsg;
  private Button mRetry;

  LoadStateViewHolder(
    @NonNull ViewGroup parent,
    @NonNull View.OnClickListener retryCallback) {
    super(LayoutInflater.from(parent.getContext())
      .inflate(R.layout.load_state_item, parent, false));

    LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView);
    mProgressBar = binding.progressBar;
    mErrorMsg = binding.errorMsg;
    mRetry = binding.retryButton;
  }

  public void bind(LoadState loadState) {
    if (loadState instanceof LoadState.Error) {
      LoadState.Error loadStateError = (LoadState.Error) loadState;
      mErrorMsg.setText(loadStateError.getError().getLocalizedMessage());
    }
    mProgressBar.setVisibility(loadState instanceof LoadState.Loading
      ? View.VISIBLE : View.GONE);
    mRetry.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
    mErrorMsg.setVisibility(loadState instanceof LoadState.Error
      ? View.VISIBLE : View.GONE);
  }
}

بعد ذلك، أنشئ فئة تنفِّذ LoadStateAdapter، وحدِّد طريقتَي onCreateViewHolder() وonBindViewHolder(). تنشئ هذه الطرق مثيلاً لصاحب العرض المخصّص وتربط حالة التحميل المرتبطة.

Kotlin

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter(
  private val retry: () -> Unit
) : LoadStateAdapter<LoadStateViewHolder>() {

  override fun onCreateViewHolder(
    parent: ViewGroup,
    loadState: LoadState
  ) = LoadStateViewHolder(parent, retry)

  override fun onBindViewHolder(
    holder: LoadStateViewHolder,
    loadState: LoadState
  ) = holder.bind(loadState)
}

Java

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> {
  private View.OnClickListener mRetryCallback;

  ExampleLoadStateAdapter(View.OnClickListener retryCallback) {
    mRetryCallback = retryCallback;
  }

  @NotNull
  @Override
  public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent,
    @NotNull LoadState loadState) {
    return new LoadStateViewHolder(parent, mRetryCallback);
  }

  @Override
  public void onBindViewHolder(@NotNull LoadStateViewHolder holder,
    @NotNull LoadState loadState) {
    holder.bind(loadState);
  }
}

Java

// Adapter that displays a loading spinner when
// state is LoadState.Loading, and an error message and retry
// button when state is LoadState.Error.
class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> {
  private View.OnClickListener mRetryCallback;

  ExampleLoadStateAdapter(View.OnClickListener retryCallback) {
    mRetryCallback = retryCallback;
  }

  @NotNull
  @Override
  public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent,
    @NotNull LoadState loadState) {
    return new LoadStateViewHolder(parent, mRetryCallback);
  }

  @Override
  public void onBindViewHolder(@NotNull LoadStateViewHolder holder,
    @NotNull LoadState loadState) {
    holder.bind(loadState);
  }
}

لعرض مستوى تقدُّم التحميل في عنوان وتذييل، استدعِ الطريقة withLoadStateHeaderAndFooter() من عنصر PagingDataAdapter:

Kotlin

pagingAdapter
  .withLoadStateHeaderAndFooter(
    header = ExampleLoadStateAdapter(adapter::retry),
    footer = ExampleLoadStateAdapter(adapter::retry)
  )

Java

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

Java

pagingAdapter
  .withLoadStateHeaderAndFooter(
    new ExampleLoadStateAdapter(pagingAdapter::retry),
    new ExampleLoadStateAdapter(pagingAdapter::retry));

يمكنك بدلاً من ذلك استدعاء withLoadStateHeader() أو withLoadStateFooter() إذا كنت تريد أن تعرض قائمة RecyclerView حالة التحميل فقط في العنوان أو في التذييل فقط.

الوصول إلى معلومات إضافية عن حالة التحميل

يوفر الكائن CombinedLoadStates من PagingDataAdapter معلومات عن حالات التحميل لتنفيذ PagingSource وأيضًا عن تنفيذ RemoteMediator، في حال توفّره.

لتسهيل الاستخدام، يمكنك استخدام السمات refresh وappend وprepend من CombinedLoadStates للوصول إلى كائن LoadState لنوع التحميل المناسب. وتتغيّر هذه السمات بشكل عام إلى حالة التحميل من تنفيذ RemoteMediator في حال توفّرها، وإلا فإنها تحتوي على حالة التحميل المناسبة من تنفيذ PagingSource. للحصول على معلومات أكثر تفصيلاً عن المنطق الأساسي، يُرجى الاطّلاع على المستندات المرجعية لـ CombinedLoadStates.

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Observe refresh load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    refreshLoadState: LoadState = loadStates.refresh
    // Observe prepend load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    prependLoadState: LoadState = loadStates.prepend
    // Observe append load state from RemoteMediator if present, or
    // from PagingSource otherwise.
    appendLoadState: LoadState = loadStates.append
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Observe refresh load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState refreshLoadState = loadStates.refresh;
  // Observe prepend load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState prependLoadState = loadStates.prepend;
  // Observe append load state from RemoteMediator if present, or
  // from PagingSource otherwise.
  LoadState appendLoadState = loadStates.append;
});

ومع ذلك، من المهم تذكُّر أنّ حالات تحميل PagingSource فقط يمكن ضمان تزامنها مع تحديثات واجهة المستخدم. بما أنّ السمات refresh وappend وprepend من المحتمل أن تأخذ حالة التحميل من PagingSource أو RemoteMediator، لا يمكن أن تكون متزامنة مع تحديثات واجهة المستخدم. يمكن أن يتسبب ذلك في حدوث مشاكل في واجهة المستخدم حيث يبدو أن التحميل ينتهي قبل إضافة أي من البيانات الجديدة إلى واجهة المستخدم.

لهذا السبب، تعمل الموصّلات الملائمة بشكل جيد لعرض حالة التحميل في رأس أو تذييل، ولكن في حالات الاستخدام الأخرى، قد تحتاج إلى الوصول تحديدًا إلى حالة التحميل من PagingSource أو RemoteMediator. توفّر السمة CombinedLoadStates السمتَين source و mediator لهذا الغرض. تكشف كل من هذه السمات عنصر LoadStates يحتوي على كائنات LoadState لـ PagingSource أو RemoteMediator على التوالي:

Kotlin

lifecycleScope.launch {
  pagingAdapter.loadStateFlow.collectLatest { loadStates ->
    // Directly access the RemoteMediator refresh load state.
    mediatorRefreshLoadState: LoadState? = loadStates.mediator.refresh
    // Directly access the RemoteMediator append load state.
    mediatorAppendLoadState: LoadState? = loadStates.mediator.append
    // Directly access the RemoteMediator prepend load state.
    mediatorPrependLoadState: LoadState? = loadStates.mediator.prepend
    // Directly access the PagingSource refresh load state.
    sourceRefreshLoadState: LoadState = loadStates.source.refresh
    // Directly access the PagingSource append load state.
    sourceAppendLoadState: LoadState = loadStates.source.append
    // Directly access the PagingSource prepend load state.
    sourcePrependLoadState: LoadState = loadStates.source.prepend
  }
}

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

Java

pagingAdapter.addLoadStateListener(loadStates -> {
  // Directly access the RemoteMediator refresh load state.
  LoadState mediatorRefreshLoadState = loadStates.mediator.refresh;
  // Directly access the RemoteMediator append load state.
  LoadState mediatorAppendLoadState = loadStates.mediator.append;
  // Directly access the RemoteMediator prepend load state.
  LoadState mediatorPrependLoadState = loadStates.mediator.prepend;
  // Directly access the PagingSource refresh load state.
  LoadState sourceRefreshLoadState = loadStates.source.refresh;
  // Directly access the PagingSource append load state.
  LoadState sourceAppendLoadState = loadStates.source.append;
  // Directly access the PagingSource prepend load state.
  LoadState sourcePrependLoadState = loadStates.source.prepend;
});

عوامل تشغيل السلسلة في LoadState

بما أنّ الكائن CombinedLoadStates يتيح الوصول إلى كل التغييرات في حالة التحميل، من المهم فلترة بث حالة التحميل بناءً على أحداث محدّدة. يضمن ذلك تحديث واجهة المستخدم في الوقت المناسب لتجنّب التقطع والتحديثات غير الضرورية لواجهة المستخدم.

على سبيل المثال، لنفترض أنك تريد عرض طريقة عرض فارغة، ولكن بعد اكتمال التحميل الأولي للبيانات فقط. تتطلّب حالة الاستخدام هذه التأكّد من بدء تحميل إعادة تحميل البيانات، ثم الانتظار إلى أن تتأكّد حالة NotLoading من اكتمال عملية إعادة التحميل. يجب تصفية جميع الإشارات باستثناء الإشارات التي تحتاجها:

Kotlin

lifecycleScope.launchWhenCreated {
  adapter.loadStateFlow
    // Only emit when REFRESH LoadState for RemoteMediator changes.
    .distinctUntilChangedBy { it.refresh }
    // Only react to cases where REFRESH completes, such as NotLoading.
    .filter { it.refresh is LoadState.NotLoading }
    // Scroll to top is synchronous with UI updates, even if remote load was
    // triggered.
    .collect { binding.list.scrollToPosition(0) }
}

Java

PublishSubject<CombinedLoadStates> subject = PublishSubject.create();
Disposable disposable =
  subject.distinctUntilChanged(CombinedLoadStates::getRefresh)
  .filter(
    combinedLoadStates -> combinedLoadStates.getRefresh() instanceof LoadState.NotLoading)
  .subscribe(combinedLoadStates -> binding.list.scrollToPosition(0));

pagingAdapter.addLoadStateListener(loadStates -> {
  subject.onNext(loadStates);
});

Java

LiveData<CombinedLoadStates> liveData = new MutableLiveData<>();
LiveData<LoadState> refreshLiveData =
  Transformations.map(liveData, CombinedLoadStates::getRefresh);
LiveData<LoadState> distinctLiveData =
  Transformations.distinctUntilChanged(refreshLiveData);

distinctLiveData.observeForever(loadState -> {
  if (loadState instanceof LoadState.NotLoading) {
    binding.list.scrollToPosition(0);
  }
});

في هذا المثال، يتم الانتظار إلى أن يتم تعديل حالة تحميل إعادة التحميل، ولكن يتم تشغيله فقط عندما تكون الحالة NotLoading. يضمن ذلك اكتمال عملية التحديث عن بُعد بشكل تام قبل إجراء أي تحديثات لواجهة المستخدم.

تتيح واجهات برمجة تطبيقات البث المباشر هذا النوع من العمليات. ويمكن لتطبيقك تحديد أحداث التحميل التي يحتاجها والتعامل مع البيانات الجديدة عند استيفاء المعايير المناسبة.