טעינה והצגה של נתונים בדפים

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

הגדרת מקור נתונים

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

משתמשים ישירות במחלקה PagingSource כדי להשתמש ב-coroutines של Kotlin לטעינה אסינכרונית. בספריית ה-Paging יש גם כיתות שתומכות במסגרות אסינכרוניות אחרות:

  • כדי להשתמש ב-RxJava, צריך להטמיע את RxPagingSource במקום זאת.
  • כדי להשתמש ב-ListenableFuture מ-Guava, צריך להטמיע את ListenableFuturePagingSource במקום זאת.

בחירת סוגי מפתחות וערכים

ל-PagingSource<Key, Value> יש שני פרמטרים מסוג: Key ו-Value. המפתח מגדיר את המזהה שמשמש לטעינת הנתונים, והערך הוא סוג הנתונים עצמם. לדוגמה, אם אתם טוענים דפים של אובייקטים מסוג User מהרשת על ידי העברת מספרי דפים מסוג Int אל Retrofit, בוחרים ב-Int בתור הסוג Key וב-User בתור הסוג Value.

הגדרת PagingSource

בדוגמה הבאה מוטמע PagingSource שטעון דפים של פריטים לפי מספר הדף. הסוג של Key הוא Int והסוג של Value הוא User.

Kotlin

class ExamplePagingSource(
    val backend: ExampleBackendService,
    val query: String
) : PagingSource<Int, User>() {
  override suspend fun load(
    params: LoadParams<Int>
  ): LoadResult<Int, User> {
    try {
      // Start refresh at page 1 if undefined.
      val nextPageNumber = params.key ?: 1
      val response = backend.searchUsers(query, nextPageNumber)
      return LoadResult.Page(
        data = response.users,
        prevKey = null, // Only paging forward.
        nextKey = response.nextPageNumber
      )
    } catch (e: Exception) {
      // Handle errors in this block and return LoadResult.Error for
      // expected errors (such as a network failure).
    }
  }

  override fun getRefreshKey(state: PagingState<Int, User>): Int? {
    // 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.
    return state.anchorPosition?.let { anchorPosition ->
      val anchorPage = state.closestPageToPosition(anchorPosition)
      anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
    }
  }
}

Java

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

Java

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, הפרמטרים שסופקו ב-constructor מועברים לשיטה load() כדי לטעון נתונים מתאימים לשאילתה. בדוגמה שלמעלה, הפרמטרים האלה הם:

  • backend: מכונה של שירות הקצה העורפי שמספק את הנתונים
  • query: שאילתת החיפוש ששולחים לשירות שצוין ב-backend

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

האובייקט LoadResult מכיל את התוצאה של פעולת הטעינה. LoadResult היא כיתה אטומה שיכולה לקבל אחת משתי צורות, בהתאם להצלחה של הקריאה ל-load():

  • אם הטעינה מסתיימת בהצלחה, מחזירים אובייקט LoadResult.Page.
  • אם הטעינה לא מסתיימת בהצלחה, מחזירים אובייקט LoadResult.Error.

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

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

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

טיפול בשגיאות

בקשות לטעינה של נתונים עלולות להיכשל מכמה סיבות, במיוחד כשהן נטענות דרך רשת. כדי לדווח על שגיאות שנתקלת בהן במהלך הטעינה, מחזירים אובייקט LoadResult.Error מהשיטה load().

לדוגמה, אפשר לתפוס ולדווח על שגיאות טעינה ב-ExamplePagingSource מהדוגמה הקודמת על ידי הוספת הקוד הבא לשיטה load():

Kotlin

catch (e: IOException) {
  // IOException for network failures.
  return LoadResult.Error(e)
} catch (e: HttpException) {
  // HttpException for any non-2xx HTTP status codes.
  return LoadResult.Error(e)
}

Java

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

Java

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:

Kotlin

val flow = Pager(
  // Configure how data is loaded by passing additional properties to
  // PagingConfig, such as prefetchDistance.
  PagingConfig(pageSize = 20)
) {
  ExamplePagingSource(backend, query)
}.flow
  .cachedIn(viewModelScope)

Java

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

Java

// 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 של מחזור החיים.

האובייקט Pager קורא לשיטה load() מהאובייקט PagingSource, ומספק לו את האובייקט LoadParams ומקבל בחזרה את האובייקט LoadResult.

הגדרת מתאם RecyclerView

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

מגדירים כיתה שמרחיבה את PagingDataAdapter. בדוגמה, UserAdapter מרחיב את PagingDataAdapter כדי לספק מתאם RecyclerView לפריטי רשימה מסוג User, תוך שימוש ב-UserViewHolder בתור מחזיק תצוגה:

Kotlin

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

Java

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

Java

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:

Kotlin

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

Java

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

Java

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

מבצעים את השלבים הבאים בשיטה onCreate של הפעילות או בשיטה onViewCreated של הפלח:

  1. יוצרים מכונה של הכיתה PagingDataAdapter.
  2. מעבירים את המופע של PagingDataAdapter לרשימה RecyclerView שבה רוצים להציג את הנתונים שמחולקים לדפים.
  3. בודקים את המקור PagingData ומעבירים כל ערך שנוצר לשיטה submitData() של המתאם.

Kotlin

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

Java

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

Java

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

מקורות מידע נוספים

מידע נוסף על ספריית הגיליון:

Codelabs