Thư viện phân trang mang đến những tính năng đắc lực để tải và hiển thị dữ liệu đã phân trang từ một tập dữ liệu lớn hơn. Hướng dẫn này minh hoạ cách sử dụng thư viện Phân trang để thiết lập luồng dữ liệu đã phân trang từ một nguồn dữ liệu mạng và hiển thị luồng đó trong một danh sách lười.
Xác định nguồn dữ liệu
Bước đầu tiên là xác định việc triển khai PagingSource để xác định nguồn dữ liệu. Lớp API PagingSource bao gồm phương thức load mà bạn phải ghi đè để cho biết cách truy xuất dữ liệu đã phân trang từ nguồn dữ liệu tương ứng.
Sử dụng trực tiếp lớp PagingSource để dùng coroutine Kotlin cho quá trình tải không đồng bộ.
Chọn loại khoá và loại giá trị
PagingSource<Key, Value> có hai tham số loại: Key và Value. Khoá xác định giá trị nhận dạng dùng để tải dữ liệu và giá trị là loại của chính dữ liệu đó. Ví dụ: Nếu tải các trang của đối tượng User từ mạng bằng cách chuyển số trang Int sang Retrofit, hãy chọn Int làm loại Key và User làm loại Value.
Xác định PagingSource
Ví dụ sau đây triển khai một PagingSource tải các trang của mục theo số trang. Loại Key là Int và loại Value là User.
class ExamplePagingSource(
val backend: ExampleBackendService,
val query: String
) : PagingSource<Int, User>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, User> {
init {
// the data source is expected to be immutable
// invalidate PagingSource if data source
// has updated
backEnd.addDatabaseOnChangedListener {
invalidate()
}
}
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 = nextPageNumber + 1
)
} 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)
}
}
}
Cách triển khai PagingSource thông thường chuyển các tham số được cung cấp trong hàm dựng
sang phương thức load để tải dữ liệu thích hợp cho một truy vấn. Trong ví dụ trên, các tham số đó là:
backend: một thực thể của dịch vụ phụ trợ cung cấp dữ liệuquery: cụm từ tìm kiếm gửi đến dịch vụ dobackendchỉ định
Đối tượng LoadParams chứa thông tin về thao tác tải định thực hiện. Dữ liệu này bao gồm
khoá cần tải và số lượng mục cần tải.
Đối tượng LoadResult
chứa kết quả của thao tác tải. LoadResult là một lớp kín ở một trong ba dạng, tuỳ vào lệnh gọi load có thành công hay không:
- Lần tải thành công trả về đối tượng
LoadResult.Page. - Lần tải không thành công trả về đối tượng
LoadResult.Error. - Nếu
PagingSourcekhông còn hợp lệ và cần được thay thế bằng một thực thể mới (ví dụ: do thay đổi dữ liệu cơ bản), hãy trả về một đối tượngLoadResult.Invalid.
Hình dưới đây minh hoạ cách hàm load trong ví dụ này nhận khoá cho mỗi lần tải và cung cấp khoá cho lần tải tiếp theo.
load sử dụng và cập nhật khoá.
Việc triển khai PagingSource cũng phải thực thi phương thức getRefreshKey lấy đối tượng PagingState làm một tham số. Phương thức này trả về khoá để chuyển vào phương thức load khi dữ liệu được làm mới hoặc không hợp lệ sau lần tải đầu tiên. Thư viện Paging tự động gọi phương thức này trong những lần làm mới dữ liệu tiếp theo.
Xử lý lỗi
Các yêu cầu tải dữ liệu có thể không thành công vì một số lý do, đặc biệt là khi tải qua mạng. Hãy báo cáo các lỗi gặp phải trong quá trình tải bằng cách trả về một đối tượng LoadResult.Error từ phương thức load.
Ví dụ: Bạn có thể nắm bắt và báo cáo các lỗi tải trong ExamplePagingSource
từ ví dụ trước bằng cách thêm nội dung sau vào phương thức load:
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)
}
Để biết thêm thông tin về cách xử lý các lỗi Retrofit, hãy xem các mẫu trong Tài liệu tham khảo API PagingSource.
PagingSource thu thập và phân phối đối tượng LoadResult.Error đến giao diện người dùng nên bạn có thể thao tác với các đối tượng đó. Để biết thêm thông tin về việc hiển thị trạng thái tải trong giao diện người dùng, hãy xem phần Quản lý và trình bày trạng thái tải.
Thiết lập luồng PagingData
Tiếp theo, bạn cần một luồng dữ liệu đã phân trang từ lần triển khai PagingSource.
Hãy thiết lập luồng dữ liệu này trong ViewModel. Lớp Pager cung cấp các phương thức hiển thị luồng phản ứng của đối tượng PagingData từ một PagingSource. Thư viện Phân trang hiển thị luồng dữ liệu dưới dạng Flow.
Khi tạo một thực thể Pager để thiết lập luồng phản ứng, bạn phải cung cấp thực thể đó với một đối tượng cấu hình PagingConfig và một hàm cho biết Pager cách tải một thực thể của hoạt động triển khai PagingSource, như trong ví dụ sau.
class UserViewModel(
private val backend: ExampleBackendService,
private val query: String
) : ViewModel() {
val userPagingFlow: Flow<PagingData<User>> = Pager(
// Configure how data is loaded by passing additional properties to
// PagingConfig, such as pageSize and enabling or disabling placeholders.
config = PagingConfig(
pageSize = 20,
enablePlaceholders = true
),
pagingSourceFactory = {
ExamplePagingSource(backend, query)
}
)
.flow
.cachedIn(viewModelScope)
}
Toán tử cachedIn giúp luồng dữ liệu có thể chia sẻ, đồng thời lưu dữ liệu đã tải vào bộ nhớ đệm với CoroutineScope được cung cấp. Nếu không có cachedIn, bạn sẽ không thể khôi phục PagingData. Ví dụ này sử dụng viewModelScope do cấu phần phần mềm vòng đời lifecycle-viewmodel-ktx cung cấp.
Đối tượng Pager gọi phương thức load từ đối tượng PagingSource, cung cấp cùng với đối tượng LoadParams và nhận đối tượng LoadResult khi trả về.
Thu thập và hiển thị dữ liệu trong giao diện người dùng
Để kết nối luồng phân trang với giao diện người dùng, hãy lấy luồng từ ViewModel rồi truyền luồng đó đến thành phần kết hợp danh sách của bạn.
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val userFlow = viewModel.userPagingFlow
UserList(flow = userFlow)
}
Dùng collectAsLazyPagingItems để chuyển đổi luồng PagingData thành LazyPagingItems. Sau đó, hãy dùng API items trong LazyColumn để bố trí từng mục.
Đảm bảo rằng bạn cung cấp một giá trị nhận dạng duy nhất và ổn định cho từng mặt hàng bằng cách sử dụng itemKey.
Ví dụ sau đây sử dụng it.id (tham chiếu đến thuộc tính User.id) vì thuộc tính này vẫn ổn định cho phiên bản User trong quá trình cập nhật dữ liệu.
@Composable
fun UserList(flow: Flow<PagingData<User>>) {
val lazyPagingItems = flow.collectAsLazyPagingItems()
LazyColumn {
items(
lazyPagingItems.itemCount,
key = lazyPagingItems.itemKey { it.id }
) { index ->
val user = lazyPagingItems[index]
if (user != null) {
UserRow(user)
} else {
UserPlaceholder()
}
}
}
}
Thư viện Phân trang sử dụng null cho các phần giữ chỗ trong khi một trang đang tải. Vì vậy, nếu đã bật phần giữ chỗ, bạn phải xử lý các giá trị null trong khối nội dung.
Giờ đây, danh sách sẽ hiển thị dữ liệu đã phân trang và Thư viện phân trang sẽ tải thêm các trang khi người dùng cuộn.
Tài nguyên khác
Để tìm hiểu thêm về thư viện Paging, hãy xem các tài nguyên khác sau đây:
Tài liệu
Xem nội dung
Đề xuất cho bạn
- Lưu ý: văn bản liên kết sẽ hiện khi JavaScript tắt
- Phân trang qua mạng và cơ sở dữ liệu
- Di chuyển sang Paging 3
- Tổng quan về thư viện Paging