คลังการแบ่งหน้ามีความสามารถในการโหลดและแสดงข้อมูลที่แบ่งหน้าจากชุดข้อมูลขนาดใหญ่ คู่มือนี้แสดงวิธีใช้ไลบรารีการแบ่งหน้าเพื่อตั้งค่าสตรีมข้อมูลที่แบ่งหน้าจากแหล่งข้อมูลเครือข่าย และแสดงใน 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()
ในตัวอย่างนี้ได้รับคีย์สําหรับการโหลดแต่ละครั้งและระบุคีย์สําหรับการโหลดครั้งถัดไป
การติดตั้งใช้งาน 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
- สร้างอินสแตนซ์ของคลาส
PagingDataAdapter
- ส่งอินสแตนซ์
PagingDataAdapter
ไปยังรายการRecyclerView
ที่ต้องการแสดงข้อมูลแบบแบ่งหน้า - สังเกตสตรีม
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
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- หน้าเว็บจากเครือข่ายและฐานข้อมูล
- ย้ายข้อมูลไปยังการแบ่งหน้า 3
- ภาพรวมไลบรารีการสร้างหน้า