إدارة حالات التحميل وعرضها (طرق العرض)

مفاهيم وتنفيذ Jetpack Compose

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

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

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

تعرض مكتبة Paging حالة التحميل لاستخدامها في واجهة المستخدم من خلال عنصر 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، يُرجى الاطّلاع على الوصول إلى معلومات إضافية عن حالة التحميل.

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

توفر مكتبة Paging محول قائمة آخر يسمى 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. يضمن ذلك اكتمال التحديث عن بُعد بالكامل قبل إجراء أي تعديلات على واجهة المستخدم.

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