ניהול והצגה של מצבי הטעינה

ספריית הדפים עוקבת אחרי המצב של בקשות הטעינה לנתונים שנשלחים לדפים וחושפת אותה דרך הכיתה LoadState. האפליקציה שלך יכולה לרשום מאזינים באמצעות PagingDataAdapter עד לקבל מידע על המצב הנוכחי ולעדכן את ממשק המשתמש בהתאם. האלה המצבים מתקבלים מהמתאם כי הם מסונכרנים עם ממשק המשתמש. כלומר, ה-listener מקבל עדכונים אחרי שטעינת הדף שהוחלו על ממשק המשתמש.

יש אות LoadState נפרד לכל סוג LoadType וסוג מקור הנתונים (PagingSource או RemoteMediator). CombinedLoadStates אובייקט שסופק על ידי ה-listener מספק מידע על מצב הטעינה מכל האותות האלה. אפשר להשתמש במידע המפורט הזה כדי להציג את מחווני טעינה שמתאימים למשתמשים שלך.

מצבי טעינה

ספריית הדפים חושפת את מצב הטעינה לשימוש בממשק המשתמש באמצעות אובייקט 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);
  }
});

הדוגמה הזו ממתינה עד שמצב עומס הרענון יופעל, אבל רק יופעל כשהמדינה (State) היא NotLoading. כך אפשר לוודא שהרענון של השלט הרחוק הסתיים באופן מלא הסתיימה לפני שיתרחשו עדכונים בממשק המשתמש.

ממשקי API של סטרימינג מאפשרים פעולה כזו. האפליקציה שלך יכולה לציין את העומס אירועים שדרושים לו, ומטפלים בנתונים החדשים כשמתקיימים הקריטריונים המתאימים.