1. Giới thiệu
Kiến thức bạn sẽ học được
- Các thành phần chính của thư viện phân trang là gì?
- Cách thêm thư viện phân trang vào dự án của bạn.
Sản phẩm bạn sẽ tạo ra
Trong lớp học lập trình này, bạn bắt đầu bằng một ứng dụng mẫu đã hiển thị danh sách các bài viết. Danh sách này ở dạng tĩnh, có 500 bài viết và tất cả đều được lưu trong bộ nhớ điện thoại:
Khi tham gia lớp học lập trình, bạn sẽ:
- ...được giới thiệu về khái niệm phân trang,
- ...được giới thiệu các thành phần chính trong Thư viện Paging.
- ...được hướng dẫn cách triển khai tính năng phân trang bằng thư viện Paging.
Khi hoàn tất, bạn sẽ có một ứng dụng:
- ...triển khai thành công tính năng phân trang.
- ...giao tiếp hiệu quả với người dùng khi hệ thống đang tìm nạp thêm dữ liệu.
Dưới đây là bản xem trước ngắn gọn về giao diện người dùng mà chúng ta sẽ tạo ra:
Bạn cần
Sẽ hữu ích hơn rất nhiều nếu bạn
- Quen thuộc với các Thành phần cấu trúc sau: ViewModel, View Binding và cấu trúc được đề xuất trong Hướng dẫn về cấu trúc ứng dụng. Để giới thiệu về Thành phần cấu trúc, hãy xem nội dung lớp học lập trình sử dụng Room cùng View (khung hiển thị).
- Làm quen với coroutine và Flow của Kotlin. Để biết thêm thông tin về Flow, hãy xem nội dung Lớp học lập trình về Coroutine nâng cao với Kotlin Flow và LiveData.
2. Thiết lập môi trường
Trong bước này, bạn sẽ tải xuống toàn bộ các đoạn mã của lớp học lập trình rồi chạy một ứng dụng mẫu đơn giản.
Để bạn có thể bắt đầu nhanh chóng, chúng tôi đã chuẩn bị một dự án khởi đầu để bạn tiếp tục xây dựng ứng dụng từ đó.
Nếu đã cài đặt git, bạn có thể chỉ cần chạy lệnh bên dưới. Để kiểm tra xem git đã được cài đặt hay chưa, hãy nhập git --version
vào dòng lệnh hoặc cửa sổ dòng lệnh và xác minh rằng mã này được thực thi đúng cách.
git clone https://github.com/googlecodelabs/android-paging
Nếu chưa có git, bạn có thể nhấp vào nút sau để tải toàn bộ các đoạn mã cho lớp học lập trình này:
Mã này được sắp xếp thành hai thư mục: basic
và advanced
. Đối với lớp học lập trình này, chúng ta chỉ quan tâm đến thư mục basic
.
Trong thư mục basic
, bạn cũng có hai thư mục khác: start
và end
. Chúng ta sẽ bắt đầu làm quen với đoạn mã trong thư mục start
và khi kết thúc lớp học lập trình này, đoạn mã trong thư mục start
phải giống với đoạn mã trong thư mục end
.
- Mở dự án ở thư mục
basic/start
trong Android Studio. - Chạy cấu hình chạy
app
trên một thiết bị hoặc một trình mô phỏng.
Chúng ta sẽ thấy một danh sách các bài viết! Cuộn xuống dưới cùng để chắc chắn rằng danh sách này ở dạng tĩnh. Nghĩa là, danh sách này sẽ không tìm nạp thêm các mục khác khi người dùng cuộn xuống cuối. Cuộn lên lại trên cùng để chắc chắn rằng toàn bộ các mục vẫn như cũ.
3. Giới thiệu về tính năng phân trang
Một trong những cách phổ biến nhất để hiển thị thông tin cho người dùng là sử dụng danh sách. Tuy nhiên, đôi khi các danh sách này chỉ hiển thị trên màn hình một phần nhỏ nội dung được cung cấp cho người dùng. Khi người dùng cuộn để lướt thông tin đã hiển thị, họ thường kỳ vọng rằng danh sách này sẽ tìm nạp thêm dữ liệu để bổ sung vào thông tin họ đã xem. Ứng dụng cần phải hiệu quả và liền mạch mỗi khi tìm nạp dữ liệu để việc tải dần dữ liệu không gây ảnh hưởng xấu đến trải nghiệm người dùng. Việc tải dần dữ liệu cũng mang lại lợi ích về mặt hiệu suất, vì ứng dụng không phải đồng thời lưu giữ một lượng lớn dữ liệu trong bộ nhớ.
Quá trình tìm nạp dần thông tin này được gọi là phân trang, trong đó mỗi trang tương ứng với một phần dữ liệu cần tìm nạp. Để yêu cầu một trang, thường thì chúng ta sẽ gửi một truy vấn chứa các thông tin cần thiết đến nguồn dữ liệu đang được phân trang. Phần còn lại của lớp học này sẽ giới thiệu về Thư viện Paging và cách thư viện này có thể giúp bạn triển khai tính năng phân trang trong ứng dụng một cách nhanh chóng và hiệu quả.
Các thành phần chính trong Thư viện Paging
Dưới đây là các thành phần chính trong Thư viện Paging:
PagingSource
– lớp cơ sở để tải các phần dữ liệu cho một truy vấn trang cụ thể. Lớp này là một phần của lớp dữ liệu (data layer) và thường hiển thị trong lớpDataSource
, sau đó đượcRepository
sử dụng trongViewModel
.PagingConfig
– một lớp quy định các thông số xác định hành vi phân trang. Thông tin này bao gồm kích thước trang, liệu phần giữ chỗ đã được bật hay chưa, v.v.Pager
– một lớp có vai trò tạo dòng tinPagingData
.PagingSource
chịu trách nhiệm thực hiện việc này và phải được tạo trongViewModel
.PagingData
– một vùng chứa dành cho dữ liệu được phân trang Mỗi lần làm mới dữ liệu sẽ có một phát xạPagingData
tương ứng được hỗ trợ bởiPagingSource
riêng.PagingDataAdapter
– một lớp conRecyclerView.Adapter
hiển thịPagingData
trongRecyclerView
.PagingDataAdapter
có thể được kết nối với KotlinFlow
,LiveData
, RxJavaFlowable
, RxJavaObservable
hoặc thậm chí là danh sách tĩnh bằng phương thức gốc.PagingDataAdapter
theo dõi các sự kiện tảiPagingData
nội bộ và cập nhật hiệu quả giao diện người dùng khi các trang được tải.
Ở những phần sau, bạn sẽ triển khai các ví dụ về từng thành phần được mô tả ở trên.
4. Tổng quan dự án
Ứng dụng ở dạng hiện tại sẽ hiển thị danh sách tĩnh các bài viết. Mỗi bài viết đều có một tiêu đề, nội dung mô tả và ngày tạo bài viết đó. Danh sách tĩnh hoạt động hiệu quả khi số lượng mục không nhiều, nhưng nó sẽ gặp khó khăn với tập dữ liệu có quy mô lớn hơn. Chúng ta sẽ khắc phục vấn đề này bằng cách triển khai tính năng phân trang bằng Thư viện Paging, nhưng trước tiên, hãy xem qua các thành phần đã có trong ứng dụng.
Ứng dụng tuân theo cấu trúc được đề xuất trong hướng dẫn về cấu trúc ứng dụng. Dưới đây là nội dung bạn sẽ tìm thấy trong mỗi gói:
Lớp dữ liệu:
ArticleRepository
: Chịu trách nhiệm cung cấp danh sách các bài viết và lưu chúng vào bộ nhớ.Article
: Một lớp đại diện cho mô hình dữ liệu, đại diện cho thông tin được lấy từ lớp dữ liệu.
Lớp giao diện người dùng:
Activity
,RecyclerView.Adapter
vàRecyclerView.ViewHolder
: Các lớp chịu trách nhiệm hiển thị danh sách trong giao diện người dùng.ViewModel
: Trình sở hữu trạng thái chịu trách nhiệm tạo trạng thái mà giao diện người dùng cần hiển thị.
Kho lưu trữ hiển thị tất cả các bài viết trong Flow
với trường articleStream
. ArticleViewModel
được đọc lần lượt trong lớp giao diện người dùng, sau đó chuẩn bị để giao diện người dùng sử dụng trong ArticleActivity
bằng trường state
, StateFlow
.
Việc hiển thị các bài viết dưới dạng Flow
từ kho lưu trữ cho phép cập nhật các bài viết được trình bày khi chúng thay đổi theo thời gian. Ví dụ: nếu tiêu đề bài viết thay đổi, thay đổi đó có thể dễ dàng được thông báo với trình thu thập articleStream
. Việc sử dụng StateFlow
cho trạng thái giao diện người dùng trong ViewModel
đảm bảo ngay cả khi chúng ta ngừng thu thập trạng thái giao diện người dùng — ví dụ: khi Activity
được tạo lại trong quá trình thay đổi cấu hình, chúng ta có thể tiếp tục ngay tại nơi đã dừng lại ở thời điểm bắt đầu thu thập lại.
Như đã đề cập trước đó, articleStream
hiện tại trong kho lưu trữ chỉ hiển thị tin tức cho ngày hiện tại. Một số người dùng chỉ có nhu cầu xem ngày hiện tại, nhưng số khác có thể muốn xem các bài viết cũ hơn khi họ cuộn qua tất cả các bài viết có sẵn cho ngày hiện tại. Hy vọng việc hiển thị các bài viết sẽ là lựa chọn lý tưởng cho tính năng phân trang. Ngoài ra còn có những lý do khác mà chúng ta nên tìm hiểu về cách phân trang thông qua các bài viết sau:
ViewModel
lưu giữ tất cả mục được tải trong bộ nhớ trongitems
StateFlow
. Đây là một mối lo ngại lớn khi tập dữ liệu thực sự lớn và nó có thể ảnh hưởng đến hiệu suất.- Việc cập nhật một hoặc nhiều bài viết trong danh sách khi những bài viết đó đã thay đổi sẽ trở nên tốn kém hơn khi danh sách bài viết càng lớn.
Thư viện Paging giúp giải quyết tất cả các vấn đề này, đồng thời cung cấp API nhất quán để tìm nạp dữ liệu tăng dần (phân trang) trong ứng dụng của bạn.
5. Xác định nguồn dữ liệu
Khi triển khai tính năng phân trang, chúng ta muốn chắc chắn các điều kiện sau được đáp ứng:
- Xử lý đúng cách các yêu cầu dữ liệu từ giao diện người dùng, đảm bảo không kích hoạt nhiều yêu cầu cùng lúc cho cùng một truy vấn.
- Lưu trữ lượng dữ liệu có thể quản lý trong bộ nhớ.
- Kích hoạt các yêu cầu tìm nạp thêm dữ liệu để bổ sung cho dữ liệu mà chúng ta đã tìm nạp.
Chúng ta có thể thực hiện tất cả những điều đó bằng PagingSource
. PagingSource
xác định nguồn dữ liệu bằng cách chỉ định cách truy xuất dữ liệu trong các đoạn tăng dần. Sau đó, đối tượng PagingData
lấy dữ liệu từ PagingSource
để phản hồi việc tải gợi ý được tạo khi người dùng cuộn trong RecyclerView
.
PagingSource
của chúng ta sẽ tải các bài viết. Trong data/Article.kt
, bạn sẽ thấy mô hình được xác định như sau:
data class Article(
val id: Int,
val title: String,
val description: String,
val created: LocalDateTime,
)
Để tạo PagingSource
, bạn sẽ cần phải xác định những thông tin sau:
- Loại khoá phân trang – Định nghĩa loại truy vấn trang mà chúng ta sử dụng để yêu cầu thêm dữ liệu. Trong trường hợp này, chúng ta sẽ tìm nạp bài viết sau hoặc trước một mã bài viết nhất định vì hệ thống sẽ đảm bảo mã của các bài viết đó được sắp xếp theo thứ tự tăng dần.
- Loại dữ liệu được tải – Mỗi trang trả về
List
bài viết, vì vậy, loại này làArticle
. - Nơi dữ liệu được truy xuất – Thông thường, đây sẽ là cơ sở dữ liệu, tài nguyên mạng hoặc bất kỳ nguồn dữ liệu được phân trang nào khác. Tuy nhiên, trong lớp học lập trình này, chúng ta sẽ sử dụng dữ liệu được tạo cục bộ.
Trong gói data
, hãy tạo một quy trình triển khai PagingSource
trong tệp mới có tên ArticlePagingSource.kt
:
package com.example.android.codelabs.paging.data
import androidx.paging.PagingSource
import androidx.paging.PagingState
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
TODO("Not yet implemented")
}
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
TODO("Not yet implemented")
}
}
PagingSource
yêu cầu chúng ta triển khai 2 hàm: load()
và getRefreshKey()
.
Hàm load()
được Thư viện phân trang gọi để tìm nạp thêm dữ liệu không đồng bộ sẽ hiển thị khi người dùng cuộn qua. Đối tượng LoadParams
lưu giữ thông tin liên quan đến thao tác tải, bao gồm:
- Khoá của trang sẽ được tải – Nếu đây là lần đầu tiên
load()
được gọi,LoadParams.key
sẽ lànull
. Trong trường hợp này, bạn sẽ phải xác định khoá trang ban đầu. Đối với dự án này, chúng ta sử dụng mã bài viết làm khoá. Hãy thêm một hằng sốSTARTING_KEY
là0
vào đầu tệpArticlePagingSource
cho khoá trang ban đầu. - Kích thước tải – số lượng mục được yêu cầu tải.
Hàm load()
trả về LoadResult
. LoadResult
có thể là một trong những loại sau đây:
LoadResult.Page
, nếu kết quả thành công.LoadResult.Error
, nếu xảy ra lỗi.LoadResult.Invalid
, nếuPagingSource
không hợp lệ vì việc đó không còn đảm bảo tính toàn vẹn của kết quả.
LoadResult.Page
có ba đối số bắt buộc:
data
: MộtList
các mục đã được tìm nạp.prevKey
: Khoá mà phương thứcload()
sử dụng khi cần tìm nạp các mục phía sau trang hiện tại.nextKey
: Khoá mà phương thứcload()
sử dụng khi cần tìm nạp các mục sau trang hiện tại.
...và 2 tuỳ chọn:
itemsBefore
: Số phần giữ chỗ sẽ hiển thị trước dữ liệu đã tải.itemsAfter
: Số phần giữ chỗ sẽ hiển thị sau dữ liệu được tải.
Khoá tải của chúng ta là trường Article.id
. Chúng ta có thể dùng mã này làm khoá vì mã nhận dạng Article
tăng thêm một lần cho mỗi bài viết; nghĩa là mã nhận dạng bài viết là những số nguyên tăng đơn điệu liên tiếp.
nextKey
hoặc prevKey
là null
nếu không có thêm dữ liệu nào được tải theo hướng tương ứng. Trong trường hợp này, đối với prevKey
:
- Nếu
startKey
giống vớiSTARTING_KEY
, chúng ta sẽ trả về giá trị null (rỗng) vì không thể tải thêm mục sau khoá này. - Hoặc chúng ta sẽ lấy mục đầu tiên trong danh sách và tải
LoadParams.loadSize
phía sau để đảm bảo không phải trả lại khoá nhỏ hơnSTARTING_KEY
. Chúng ta thực hiện việc này bằng cách xác định phương thứcensureValidKey()
.
Thêm hàm sau để kiểm tra xem phím phân trang có hợp lệ không:
class ArticlePagingSource : PagingSource<Int, Article>() {
...
/**
* Makes sure the paging key is never less than [STARTING_KEY]
*/
private fun ensureValidKey(key: Int) = max(STARTING_KEY, key)
}
Đối với nextKey
:
- Vì chúng ta hỗ trợ tải các mục vô hạn, nên sẽ truyền vào
range.last + 1
.
Ngoài ra, mỗi bài viết có một trường created
nên cũng cần tạo giá trị cho trường đó. Thêm phần dưới đây vào đầu tệp:
private val firstArticleCreatedTime = LocalDateTime.now()
class ArticlePagingSource : PagingSource<Int, Article>() {
...
}
Chúng ta hiện có thể triển khai hàm load()
với tất cả các mã đó:
import kotlin.math.max
...
private val firstArticleCreatedTime = LocalDateTime.now()
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
// Start paging with the STARTING_KEY if this is the first load
val start = params.key ?: STARTING_KEY
// Load as many items as hinted by params.loadSize
val range = start.until(start + params.loadSize)
return LoadResult.Page(
data = range.map { number ->
Article(
// Generate consecutive increasing numbers as the article id
id = number,
title = "Article $number",
description = "This describes article $number",
created = firstArticleCreatedTime.minusDays(number.toLong())
)
},
// Make sure we don't try to load items behind the STARTING_KEY
prevKey = when (start) {
STARTING_KEY -> null
else -> ensureValidKey(key = range.first - params.loadSize)
},
nextKey = range.last + 1
)
}
...
}
Tiếp theo, chúng ta cần triển khai getRefreshKey()
. Phương thức này được gọi khi Thư viện phân trang cần tải lại các mục cho giao diện người dùng vì dữ liệu trong phương thức sao lưu PagingSource
đã thay đổi. Trong trường hợp này, dữ liệu cơ sở cho PagingSource
đã thay đổi và cần được cập nhật trong giao diện người dùng không hợp lệ. Khi đã hết hiệu lực, Thư viện Paging sẽ tạo một PagingSource
mới để tải lại dữ liệu và thông báo cho giao diện người dùng bằng cách phát ra PagingData
mới. Chúng ta sẽ tìm hiểu thêm về trường hợp không hợp lệ ở phần sau.
Khi tải từ PagingSource
mới, getRefreshKey()
được gọi để cung cấp khoá mà PagingSource
mới bắt đầu tải cùng để đảm bảo người dùng không bị mất vị trí hiện tại trong danh sách sau khi làm mới.
Việc vô hiệu hoá trong thư viện phân trang xảy ra vì một trong hai lý do sau:
- Bạn đã gọi
refresh()
trênPagingAdapter
. - Bạn đã gọi
invalidate()
trênPagingSource
.
Khoá được trả về (trong trường hợp của chúng ta là Int
) sẽ được chuyển sang lệnh gọi tiếp theo của phương thức load()
trong PagingSource
mới thông qua đối số LoadParams
. Để ngăn các mục nhảy xung quanh sau khi hết hiệu lực, chúng ta cần đảm bảo khoá được trả về sẽ tải đủ mục để lấp đầy màn hình. Điều này làm tăng khả năng tập hợp các mục mới bao gồm các mục đã có trong dữ liệu không hợp lệ, giúp duy trì vị trí cuộn hiện tại. Hãy xem cách triển khai trong ứng dụng của chúng ta:
// The refresh key is used for the initial load of the next PagingSource, after invalidation
override fun getRefreshKey(state: PagingState<Int, Article>): Int? {
// In our case we grab the item closest to the anchor position
// then return its id - (state.config.pageSize / 2) as a buffer
val anchorPosition = state.anchorPosition ?: return null
val article = state.closestItemToPosition(anchorPosition) ?: return null
return ensureValidKey(key = article.id - (state.config.pageSize / 2))
}
Trong đoạn mã trên, chúng ta sử dụng PagingState.anchorPosition
. Nếu bạn muốn biết làm thế nào thư viện phân trang biết cách tìm nạp thêm mục thì đây là một gợi ý! Khi giao diện người dùng cố gắng đọc các mục từ PagingData
, nó sẽ cố đọc tại một chỉ mục nhất định. Khi dữ liệu được đọc, dữ liệu đó sẽ được hiển thị trong giao diện người dùng. Tuy nhiên, nếu không có dữ liệu thì thư viện phân trang sẽ biết cần phải tìm nạp dữ liệu để thực hiện yêu cầu đọc không thành công. Chỉ mục cuối cùng đã tìm nạp dữ liệu thành công khi đọc là anchorPosition
.
Khi làm mới, chúng ta lấy khoá của Article
gần nhất với anchorPosition
để dùng làm khoá tải. Theo đó, khi chúng ta bắt đầu tải lại từ PagingSource
mới, tập hợp các mục đã tìm nạp bao gồm các mục đã được tải nhằm đảm bảo trải nghiệm người dùng suôn sẻ và nhất quán.
Vậy là bạn đã xác định đầy đủ về PagingSource
. Bước tiếp theo là kết nối nó với giao diện người dùng.
6. Tạo PagingData cho giao diện người dùng
Trong quá trình triển khai hiện tại, chúng ta dùng Flow<List<Article>>
trong ArticleRepository
để hiển thị dữ liệu đã tải cho ViewModel
. Đổi lại, ViewModel
sẽ duy trì trạng thái dữ liệu luôn có sẵn với toán tử stateIn
để hiển thị trên giao diện người dùng.
Thay vào đó, chúng ta sẽ hiển thị một Flow<PagingData<Article>>
từ ViewModel
với Thư viện Paging. PagingData
là một loại bọc dữ liệu mà chúng ta đã tải và giúp Thư viện Paging quyết định thời điểm tìm nạp thêm dữ liệu, đồng thời đảm bảo việc chúng ta không yêu cầu hai lần cho cùng một trang.
Để tạo PagingData
, chúng ta sẽ dùng một trong các phương thức trình tạo từ lớp Pager
tuỳ thuộc vào API mà chúng ta muốn dùng để chuyển PagingData
đến các lớp khác của ứng dụng:
- Kotlin
Flow
– sử dụngPager.flow
. LiveData
– sử dụngPager.liveData
.- RxJava
Flowable
– sử dụngPager.flowable
. - RxJava
Observable
– sử dụngPager.observable
.
Vì đã sử dụng Flow
trong ứng dụng nên chúng ta sẽ tiếp tục áp dụng phương pháp này; nhưng thay vì dùng Flow<List<Article>>
, chúng ta sẽ dùng Flow<PagingData<Article>>
.
Bất kể bạn tạo trình tạo PagingData
nào, bạn cũng phải chuyển các thông số sau:
PagingConfig
. Lớp này sẽ đặt các tuỳ chọn liên quan đến cách tải nội dung từPagingSource
, chẳng hạn như mức tải trước, yêu cầu kích thước cho lần tải ban đầu và các tuỳ chọn khác. Tham số bắt buộc duy nhất bạn phải xác định là kích thước trang — số lượng mục phải tải trong mỗi trang. Theo mặc định, tính năng Paging sẽ giữ tất cả các trang bạn tải trong bộ nhớ. Để đảm bảo bạn không lãng phí bộ nhớ khi người dùng cuộc di chuyển, hãy đặt tham sốmaxSize
trongPagingConfig
. Theo mặc định, tính năng Paging sẽ trả về các mục null (rỗng) làm trình giữ chỗ cho nội dung chưa tải nếu tính năng Paging có thể tính các mục chưa tải và nếu cờ cấu hìnhenablePlaceholders
làtrue
. Bằng cách đó, bạn có thể hiển thị chế độ xem trình giữ chỗ trong bộ chuyển đổi. Để đơn giản hoá nội dung trong lớp học lập trình này, hãy tắt trình giữ chỗ bằng cách truyềnenablePlaceholders = false
.- Một hàm xác định cách tạo
PagingSource
. Trong trường hợp này, chúng ta sẽ tạo mộtArticlePagingSource
, vì vậy cần có một hàm giúp cho Thư viện Paging biết cách thực hiện việc đó.
Hãy chỉnh sửa ArticleRepository
của chúng ta!
Cập nhật ArticleRepository
- Xoá trường
articlesStream
. - Thêm một phương thức có tên là
articlePagingSource()
để trả vềArticlePagingSource
chúng ta vừa tạo.
class ArticleRepository {
fun articlePagingSource() = ArticlePagingSource()
}
Dọn dẹp ArticleRepository
Thư viện Paging hỗ trợ chúng ta với rất nhiều chức năng:
- Xử lý bộ nhớ đệm trong bộ nhớ.
- Yêu cầu dữ liệu khi người dùng ở gần cuối danh sách.
Điều này có nghĩa mọi nội dung khác trong ArticleRepository
đều có thể bị xoá, ngoại trừ articlePagingSource()
. Bây giờ, tệp ArticleRepository
của bạn sẽ có dạng như sau:
package com.example.android.codelabs.paging.data
import androidx.paging.PagingSource
class ArticleRepository {
fun articlePagingSource() = ArticlePagingSource()
}
Bạn hiện sẽ có lỗi biên dịch trong ArticleViewModel
. Hãy xem cần phải thực hiện những thay đổi nào ở đó!
7. Yêu cầu và lưu bộ nhớ đệm PagingData trong ViewModel
Trước khi giải quyết các lỗi biên dịch, hãy xem lại ViewModel
.
class ArticleViewModel(...) : ViewModel() {
val items: StateFlow<List<Article>> = ...
}
Để tích hợp thư viện Paging trong ViewModel
, chúng ta sẽ thay đổi loại dữ liệu trả về items
từ StateFlow<List<Article>>
thành Flow<PagingData<Article>>
. Để làm việc này, trước tiên hãy thêm một hằng số riêng tư có tên là ITEMS_PER_PAGE
vào đầu tệp:
private const val ITEMS_PER_PAGE = 50
class ArticleViewModel {
...
}
Tiếp theo, chúng ta cập nhật items
thành kết quả đầu ra của thực thể Pager
. Chúng ta làm điều này bằng cách chuyển tới Pager
hai tham số:
PagingConfig
cópageSize
làITEMS_PER_PAGE
và trình giữ chỗ đã bị tắtPagingSourceFactory
cung cấp một bản sao củaArticlePagingSource
mà chúng ta vừa tạo.
class ArticleViewModel(...) : ViewModel() {
val items: Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
pagingSourceFactory = { repository.articlePagingSource() }
)
.flow
...
}
Tiếp theo, để duy trì trạng thái phân trang thông qua các thay đổi về cấu hình hoặc điều hướng, chúng ta sử dụng phương thức cachedIn()
truyền hàm đó androidx.lifecycle.viewModelScope
.
Sau khi hoàn tất những thay đổi nêu trên, ViewModel
của chúng ta sẽ có dạng như sau:
package com.example.android.codelabs.paging.ui
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.example.android.codelabs.paging.data.Article
import com.example.android.codelabs.paging.data.ArticleRepository
import com.example.android.codelabs.paging.data.ITEMS_PER_PAGE
import kotlinx.coroutines.flow.Flow
private const val ITEMS_PER_PAGE = 50
class ArticleViewModel(
private val repository: ArticleRepository,
) : ViewModel() {
val items: Flow<PagingData<Article>> = Pager(
config = PagingConfig(pageSize = ITEMS_PER_PAGE, enablePlaceholders = false),
pagingSourceFactory = { repository.articlePagingSource() }
)
.flow
.cachedIn(viewModelScope)
}
Một lưu ý khác nữa về PagingData
: Đây là loại vùng chứa độc lập chứa một luồng cập nhật có thể thay đổi cho dữ liệu sẽ hiển thị trong RecyclerView
. Mỗi lượt phát PagingData
là hoàn toàn độc lập, và nhiều thực thể PagingData
có thể được phát ra cho một truy vấn nếu PagingSource
sao lưu không hợp lệ do các thay đổi trong tập dữ liệu cơ bản. Do đó, Flows
/PagingData
phải được hiển thị độc lập với Flows
khác.
Vậy là xong! Chúng ta hiện có chức năng phân trang trong ViewModel
!
8. Thiết lập để Bộ chuyển đổi hoạt động với PagingData
Để liên kết PagingData
với một RecyclerView
, hãy sử dụng PagingDataAdapter
. PagingDataAdapter
sẽ nhận được thông báo bất cứ khi nào nội dung PagingData
được tải, sau đó sẽ báo hiệu RecyclerView
để cập nhật.
Cập nhật ArticleAdapter
để hoạt động với luồng PagingData
:
- Hiện tại,
ArticleAdapter
sẽ triển khaiListAdapter
. Thay vào đó, hãy triển khaiPagingDataAdapter
. Phần còn lại của phần nội dung lớp học sẽ không thay đổi:
import androidx.paging.PagingDataAdapter
...
class ArticleAdapter : PagingDataAdapter<Article, RepoViewHolder>(ARTICLE_DIFF_CALLBACK) {
// body is unchanged
}
Chúng ta đã thực hiện rất nhiều thay đổi cho đến thời điểm này, nhưng giờ chỉ cần thực hiện một bước nữa thôi là có thể chạy ứng dụng – chúng ta cần kết nối giao diện người dùng!
9. Sử dụng PagingData trong giao diện người dùng
Trong quá trình triển khai hiện tại, chúng ta đã có phương thức với tên binding.setupScrollListener()
. Phương thức này gọi ViewModel
để tải thêm dữ liệu nếu đáp ứng một số điều kiện nhất định. Thư viện Paging sẽ tự động thực hiện tất cả những việc đó, vì vậy, chúng ta có thể xoá phương thức này cũng như dữ liệu sử dụng của nó.
Tiếp theo, do ArticleAdapter
không còn làListAdapter
mà là PagingDataAdapter
, chúng ta sẽ thực hiện 2 thay đổi nhỏ:
- Chúng ta chuyển đổi toán tử đầu cuối trên
Flow
từViewModel
sangcollectLatest
thay vìcollect
. - Chúng t sẽ thông báo cho
ArticleAdapter
về những thay đổi vớisubmitData()
thay vìsubmitList()
.
Chúng ta sử dụng collectLatest
trên pagingData
Flow
để việc thu thập dữ liệu phát pagingData
trước đó bị huỷ khi phát một thực thể pagingData
mới.
Với những thay đổi đó, Activity
sẽ có dạng như sau:
import kotlinx.coroutines.flow.collectLatest
class ArticleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityArticlesBinding.inflate(layoutInflater)
val view = binding.root
setContentView(view)
val viewModel by viewModels<ArticleViewModel>(
factoryProducer = { Injection.provideViewModelFactory(owner = this) }
)
val items = viewModel.items
val articleAdapter = ArticleAdapter()
binding.bindAdapter(articleAdapter = articleAdapter)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
items.collectLatest {
articleAdapter.submitData(it)
}
}
}
}
}
private fun ActivityArticlesBinding.bindAdapter(
articleAdapter: ArticleAdapter
) {
list.adapter = articleAdapter
list.layoutManager = LinearLayoutManager(list.context)
val decoration = DividerItemDecoration(list.context, DividerItemDecoration.VERTICAL)
list.addItemDecoration(decoration)
}
Ứng dụng hiện sẽ biên dịch và khởi chạy. Bạn đã chuyển thành công ứng dụng sang Thư viện Paging!
10. Hiển thị các trạng thái tải trong giao diện người dùng
Khi Thư viện Paging đang tìm nạp thêm mục để hiển thị trong giao diện người dùng, phương pháp hay nhất là cho người dùng biết rằng có nhiều dữ liệu hơn đang được chuyển đến. May mắn là thư viện Paging cung cấp cách thuận tiện để truy cập trạng thái tải với loại CombinedLoadStates
.
Các thực thể CombinedLoadStates
mô tả trạng thái tải của tất cả thành phần trong Thư viện Paging có tải dữ liệu. Trong trường hợp này, chúng ta chỉ quan tâm đến LoadState
trong ArticlePagingSource
, vì vậy chúng ta sẽ thao tác chủ yếu với loại LoadStates
trong trường CombinedLoadStates.source
. Bạn có thể truy cập CombinedLoadStates
qua PagingDataAdapter
thông qua PagingDataAdapter.loadStateFlow
.
CombinedLoadStates.source
là một loại LoadStates
, với các trường cho 3 loại LoadState
khác nhau:
LoadStates.append
: Đối vớiLoadState
mục được tìm nạp sau vị trí hiện tại của người dùng.LoadStates.prepend
: Dành cho mụcLoadState
được tìm nạp trước vị trí hiện tại của người dùng.LoadStates.refresh
: Đối vớiLoadState
của tải ban đầu.
Mỗi LoadState
có thể có một trong các trạng thái sau:
LoadState.Loading
: Các mục đang được tải.LoadState.NotLoading
: Các mục chưa được tải.LoadState.Error
: Đã xảy ra lỗi khi tải.
Trong trường hợp này, chúng ta chỉ quan tâm nếu LoadState
là LoadState.Loading
vì ArticlePagingSource
không bao gồm trường hợp lỗi.
Điều đầu tiên cần làm là thêm thanh tiến trình vào đầu và cuối giao diện người dùng để cho biết trạng thái tải cho các lần tìm nạp theo một trong hai hướng.
Trong activity_articles.xml
, hãy thêm hai thanh LinearProgressIndicator
như sau:
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.ArticleActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="0dp"
android:layout_height="0dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/prepend_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/append_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:indeterminate="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Tiếp theo, chúng ta tương tác với CombinedLoadState
bằng cách thu thập LoadStatesFlow
từ PagingDataAdapter
. Thu thập trạng thái trong ArticleActivity.kt
:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
articleAdapter.loadStateFlow.collect {
binding.prependProgress.isVisible = it.source.prepend is Loading
binding.appendProgress.isVisible = it.source.append is Loading
}
}
}
lifecycleScope.launch {
...
}
Cuối cùng, chúng ta thêm độ trễ trong ArticlePagingSource
để mô phỏng tải:
private const val LOAD_DELAY_MILLIS = 3_000L
class ArticlePagingSource : PagingSource<Int, Article>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Article> {
val start = params.key ?: STARTING_KEY
val range = startKey.until(startKey + params.loadSize)
if (start != STARTING_KEY) delay(LOAD_DELAY_MILLIS)
return ...
}
Chạy ứng dụng một lần nữa và di chuyển xuống cuối danh sách. Bạn sẽ thấy thanh tiến trình ở dưới cùng hiển thị trong khi thư viện phân trang tìm nạp thêm mục và biến mất khi hoàn tất!
11. Kết thúc
Cùng tóm tắt nhanh những nội dung chúng ta đã đề cập. Chúng ta....
- ... đã khám phá tổng quan về tính năng phân trang và tại sao nó quan trọng.
- ... đã thêm tính năng phân trang vào ứng dụng bằng cách tạo một
Pager
, xác địnhPagingSource
và phátPagingData
. - ... đã lưu vào bộ nhớ đệm
PagingData
trongViewModel
bằng toán tửcachedIn
. - ... đã sử dụng
PagingData
trong giao diện người dùng bằng cách sử dụngPagingDataAdapter
. - ...đã dùng
PagingDataAdapter.loadStateFlow
tương tác vớiCombinedLoadStates
.
Vậy là xong! Để xem các khái niệm phân trang nâng cao khác, hãy tham khảo nội dung lớp học lập trình về Paging nâng cao!