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 danh sách tải từng phần.
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, bạn sẽ 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. Để phân biệt các loại tải trong load, hãy kiểm tra xem lớp con LoadParams nào sau đây đã được truyền: LoadParams.Refresh, LoadParams.Append hoặc LoadParams.Prepend.
Đố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. TruyềnnullchonextKeyhoặcprevKeynếu không thể tải thêm dữ liệu theo hướng đó. - 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ệ do dữ liệu cơ sở đã thay đổi, hãy trả về đố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. Đối với tính năng phân trang dựa trên độ lệch, trong đó các khoá đại diện cho vị trí của mục, bạn có thể tính toán khoá làm mới tương ứng với state.anchorPosition:
// Only for positional or offset PagingSources where Key is a 0-based item index
// rather than a page number.
override fun getRefreshKey(state: PagingState<Int, User>): Int? {
// Center the refreshed load around the user's current viewport so the list
// does not jump back to the top on invalidation.
return state.anchorPosition?.let { anchorPosition ->
(anchorPosition - state.config.initialLoadSize / 2).coerceAtLeast(0)
}
}
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à hiển thị 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. Bạn cũng có thể chỉ định một initialKey không bắt buộc, chẳng hạn như 1 cho số trang dựa trên 1 hoặc 0 cho phân trang dựa trên độ lệch. Trong PagingConfig, bạn có thể định cấu hình pageSize, initialLoadSize, prefetchDistance và enablePlaceholders. Thuộc tính prefetchDistance xác định số lượng mục từ một trong hai cạnh của nội dung đã tải sẽ kích hoạt lần tìm nạp tiếp theo.
class UserViewModel(
private val backend: ExampleBackendService,
private val query: String
) : ViewModel() {
val userPagingFlow: Flow<PagingData<User>> = Pager(
config = PagingConfig(
pageSize = 20,
// Load 3 pages initially so the user can scroll immediately without
// hitting a loading state.
initialLoadSize = 60,
// Fetch the next page far enough ahead of the scroll position to
// avoid visible pauses.
prefetchDistance = 20,
enablePlaceholders = true
),
initialKey = 1,
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 không thể nhớ lại 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 và truyền luồng đó đến thành phần composable danh sách.
@Composable
fun UserScreen(viewModel: UserViewModel = viewModel()) {
val userFlow = viewModel.userPagingFlow
UserList(flow = userFlow)
}
Sử dụng collectAsLazyPagingItems để chuyển đổi luồng PagingData thành LazyPagingItems. Sau đó, hãy sử dụng API items trong LazyColumn để bố trí từng mục.
Hãy nhớ cung cấp một giá trị nhận dạng duy nhất và ổn định cho mỗi mục 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 giữ nguyên cho thực thể User trong các bản 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 phần giữ chỗ trong khi tải trang, 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.
Bây giờ, 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
Nội dung lượt xem
Đề 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