โหลดและแสดงข้อมูลการแบ่งหน้า

คลังการแบ่งหน้ามีความสามารถในการโหลดและแสดงข้อมูลที่แบ่งหน้าจากชุดข้อมูลขนาดใหญ่ คู่มือนี้แสดงวิธีใช้ไลบรารีการแบ่งหน้าเพื่อตั้งค่าสตรีมข้อมูลที่แบ่งหน้าจากแหล่งข้อมูลเครือข่าย และแสดงใน RecyclerView

กําหนดแหล่งข้อมูล

ขั้นตอนแรกคือการกําหนดการติดตั้งใช้งาน PagingSource เพื่อระบุแหล่งข้อมูล คลาส PagingSource API มีเมธอด load() ซึ่งคุณลบล้างเพื่อระบุวิธีเรียกข้อมูลแบบแบ่งหน้าจากแหล่งข้อมูลที่เกี่ยวข้อง

ใช้คลาส PagingSource โดยตรงเพื่อใช้โคโริวทีนของ Kotlin สำหรับการโหลดแบบแอซิงค์ ไลบรารีการแบ่งหน้ายังมีคลาสที่รองรับเฟรมเวิร์กแบบแอสซิงค์อื่นๆ ดังต่อไปนี้

  • หากต้องการใช้ RxJava ให้ใช้ RxPagingSource แทน
  • หากต้องการใช้ ListenableFuture จาก Guava ให้ใช้ ListenableFuturePagingSource แทน

เลือกประเภทคีย์และค่า

PagingSource<Key, Value>มีพารามิเตอร์ประเภท 2 รายการ ได้แก่ 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 ทั่วไปจะส่งพารามิเตอร์ที่ระบุไว้ในคอนสตรัคเตอร์ไปยังเมธอด 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 ไปยัง UI เพื่อให้คุณดำเนินการกับออบเจ็กต์เหล่านั้นได้ ดูข้อมูลเพิ่มเติมเกี่ยวกับการแสดงสถานะการโหลดใน UI ได้ที่จัดการและแสดงสถานะการโหลด

ตั้งค่าสตรีมของ PagingData

ถัดไป คุณต้องมีสตรีมข้อมูลที่แบ่งหน้าจากการติดตั้งใช้งาน PagingSource ตั้งค่าสตรีมข้อมูลใน ViewModel คลาส Pager มีเมธอดที่แสดงสตรีมแบบเรียลไทม์ของออบเจ็กต์ PagingData จาก PagingSource ไลบรารีการแบ่งหน้ารองรับการใช้สตรีมหลายประเภท ซึ่งรวมถึง 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 รายการ ไลบรารีการแบ่งหน้ามีคลาส 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 list adapters

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

แสดงข้อมูลที่แบ่งหน้าใน UI

เมื่อกําหนด 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