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

מושגים ויישום ב-Jetpack פיתוח נייטיב

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

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

מצבי טעינה

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

  • אם אין פעולת טעינה פעילה ואין שגיאה, אז LoadState הוא אובייקט LoadState.NotLoading. מחלקת המשנה הזו כוללת גם את המאפיין endOfPaginationReached, שמציין אם הגעתם לסוף של חלוקת הדפים.
  • אם יש פעולת טעינה פעילה, אז LoadState הוא אובייקט LoadState.Loading.
  • אם יש שגיאה, אז LoadState הוא אובייקט LoadState.Error.

יש שתי דרכים להשתמש ב-LoadState בממשק המשתמש: באמצעות listener או באמצעות מתאם רשימה מיוחד להצגת מצב הטעינה ישירות ברשימה 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(). השיטות האלה יוצרות מופע של placeholder לתצוגה בהתאמה אישית ומקשרות את מצב הטעינה המשויך.

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

כדי להציג את התקדמות הטעינה בכותרת ובכותרת התחתונה, קוראים ל-method‏ 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. כך מוודאים שהרענון מרחוק הסתיים באופן מלא לפני שמתבצעים עדכונים בממשק המשתמש.

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