Viblo Learning
+4

Android Architecture Components - Paging Library

Bài toán đưa ra đó là chúng ta muốn load data từ DB hay API để hiển thị lên UI. Nếu bạn không cẩn thận thì bạn sẽ load cả những data mà thực tế bạn không cần chúng, gây lãng phí pin và băng thông, nếu dữ liệu được cập nhật liên tục nó khó có thể đồng bộ hóa giao diện người dùng. Và yêu cầu đưa ra là chỉ cần load và hiển thị lượng nhỏ dữ liệu lên UI thôi. Mặc dù các API hiện có được cho phép phân trang nội dung:

  • CursorAdapter: giúp dễ dàng hơn trong việc ánh xạ các kết quả truy vấn cơ sở dữ liệu tới ListView , nhưng nó chạy các truy vấn cơ sở dữ liệu trên UI thread và nội dung trang không hiệu quả với một Cursor.
  • AsyncListUtil: cho phép phân trang dữ liệu dựa trên vị trí tới RecyclerView , nhưng không cho phép phân trang không có vị trí, và nó buộc các giá trị null-as-placeholders trong một tập dữ liệu có thể đếm được.
  • Hay dùng nhất là EndlessRecyclerOnScrollListener, với EndlessRecyclerOnScrollListener extend RecyclerView.OnScrollListener().: với class này thì nó cũng có một số hạn chế đáng kể. Nó tính toán Load more phụ thuộc vào first visible item, visible item hay total item của RecyclerView, và dựa vào hàm onScroll để xử lý dẫn đến hiệu năng thấp. Nếu như cài đặt code không cẩn thận thì có thể dẫn đến một số sai sót, cần chỉ rõ LayoutManager muốn support. Nếu người dùng cuộn lên xuống nhanh chóng, họ có thể sẽ tải gấp đôi nội dung mới, do đó bạn phải theo dõi trạng thái để đảm bảo rằng bạn chưa tải thông tin mới. Nếu bạn không quản lý logic trạng thái được đề cập trước đó, bạn có thể bị không tải bất cứ thứ gì hoặc tải hai bản sao của trang dữ liệu tiếp theo.

Paging sẽ khắc phục được những điểm yếu này, chúng ta cùng tìm hiểu nhé!

1. Paging là gì?

Paging là một thư viện cho phép bạn tải và hiển thị một phần nhỏ dữ liệu tại một thời điểm. Thư viện này chứa một số lớp để sắp xếp hợp lý quá trình yêu cầu dữ liệu khi bạn cần. Các lớp này cũng làm việc liền mạch với các component hiện có, như Room. Thư viện này còn hỗ trợ cả những list lớn, giới hạn và không giới hạn. Nó có PagedStorage để cache data. Nó cũng cung cấp tích hợp với RecyclerView thường dùng để hiển thị tập dữ liệu lớn và nó cũng hỗ trợ dùng với LiveData hoặc RxJava cho việc quan sát dữ liệu mới trên UI.

2. Các thành phần của Paging.

Các thành phần của Paging:

  • DataSource: Class phụ trách việc tải dữ liệu. Ví dụ app của chúng ta tải dữ liệu từ backend api hay local database
  • PagedList: List wrapper, chứa trong nó là danh sách dữ liệu thực cần hiển thị, PagedList sẽ chứa dữ liệu từ Datasource tải xuống. Bạn có thể xác định số lượng item được tải trong lần đầu tiên, hoặc lượng dữ liệu được tải trong cùng một lúc giảm thiểu lượng thời gian người dùng của bạn phải chờ dữ liệu được tải. Lớp này có thể cung cấp các tín hiệu cập nhật cho các lớp khác, chẳng hạn như RecyclerView.Adapter , cho phép bạn cập nhật nội dung của RecyclerView khi dữ liệu được tải trong các trang.
  • PagedList Adapter: PagedList sau khi có dữ liệu thì sẽ được truyền cho adapter để hiển thị. nó lắng nghe các callback để tải PagedList khi các page được tải và sử dụng DiffUtil trên background thread để tính toán và cập nhật dữ liệu khi có sự thay đổi. Lớp PagedListAdapter là một implement của RecyclerView.Adapter.

Khi code với Paging: Trong DAO:

@Dao
interface CheeseDao {

    /**
     * Room knows how to return a LivePagedListProvider, from which we can get a LiveData and serve
     * it back to UI via ViewModel.
     */
    @Query("SELECT * FROM Cheese ORDER BY name COLLATE NOCASE ASC")
    fun allCheeseByName(): DataSource.Factory<Int, Cheese>
}

Ở đây, khi bạn khai báo như thế này thì Room tự gen ra một Factory của PositionalDatasource.

getData:

private val dao = CheeseDb.get(app).cheeseDao()

    val allCheese = dao.allCheeseByName().toLiveData(
        Config(
            pageSize = 10,
            enablePlaceholders = true, // default = true
            initialLoadSizeHint = 20, // default = 3*pageSize
            prefetchDistance = 3, // default = pageSize.
            maxSize = 200
        )
    )

Trong đoạn config trên thì có một số thuộc tính cần chú ý:

  • pageSize: số lượng item load cho 1 page trong 1 lần từ DataSource.

  • enablePlaceholders: PagedList sẽ hiển thị null placeholders cho những item mà chưa được load content, mặc định nó sẽ là true.

  • initialLoadSizeHint: số lượng item trong lần load đầu tiên, nếu không set thì mặc định nó bằng 3 lần pageSize. Khi sử dụng PositionalDataSource thì thông số này phải lớn hơn PageSize.

  • prefetchDistance: xác định khoảng cách(số item) từ phần nội dung đã được tải để load tiếp data, nếu không set thì mặc định nó bằng pageSize.

  • maxSize: số lượng item tối đa được lưu trong PagedStorage.

Khai báo Adapter:

class CheeseAdapter: PagedListAdapter<Cheese, CheeseAdapter.CheeseViewHolder>(diffCallback) {
}
  • Ở đây sử dụng PagedListAdapter thay vì sử dụng ListAdapter, tìm hiểu sâu trong class này thì sẽ thấy rõ cơ chế load more của thư viện này (dựa vào hàm getItem()). Mình sẽ nói sơ qua một chút, từ class Adapter mình có gọi tới PagedListAdapter.getItem(position) để bind dữ liệu vào view. Trong hàm này cần truyền vào vị trí của item và gọi tới AsyncPagedListDiffer.getItem(position), trong hàm này, nó không chỉ trả về item tại position mà còn xử lý hàm PagedList.loadAround(index), hàm này sẽ gián tiếp tính toán xử lý load more dựa trên position của item trong RecyclerView.

Hiển thị dữ liệu lên UI:

viewModel.allCheese.observe(this, Observer {
            adapter.submitList(it)
        })

Ở đây, khi gọi submitList thì dữ liệu sẽ được cập nhật lên UI.

3. Các loại DataSource

Có 3 loại data source như sau:

  • PageKeyedDataSource: lấy page làm key để load data từ trong DataSource, nếu bạn cần sử dụng data từ page N-1 để load dữ liệu cho page N và dữ liệu ở đây đã được phân trang theo chỉ số page rồi.
class PageKeyedDataSource : PageKeyedDataSource<Int, Movie>() {

    override fun loadInitial(
        params: LoadInitialParams<Int>,
        callback: LoadInitialCallback<Int, Movie>
    ) {
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Movie>) {
        //ignore
    }
}
  • ItemKeyedDataSource: lấy chính Item làm key để load data từ trong DataSource, nếu bạn cần sử dụng data từ page N-1 để load dữ liệu cho page N. Ví dụ lần trước Paging tải dữ liệu cho item thứ 10, lần sau nó sẽ tự động tải tiếp từ item thứ 11.
/**
 * A data source that uses the "name" field of posts as the key for next/prev pages.
 */
class ItemKeyedSubredditDataSource: ItemKeyedDataSource<String, RedditPost>() {
    
    override fun loadInitial(
        params: LoadInitialParams<String>,
        callback: LoadInitialCallback<RedditPost>
    ) {
    }

    override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<RedditPost>) {
    }

    override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<RedditPost>) {
        //ignore
    }

    override fun getKey(item: RedditPost): String = item.name
}
  • PositionalDataSource: tải dữ liệu có kích thước cố định, có thể đếm được, hỗ trợ tải kích thước cố định tại các vị trí trang tùy ý. Việc cố định data như vậy cho phép Paging truy cập từng phần tử một các chính xác nhất.

4. Kiến trúc data mà Paging support

Paging Library hỗ trợ những kiểu kiến trúc dữ liệu sau:

  • Network only: Với dữ liệu từ backend server, nên sử dụng Retrofit để load dữ liệu vào DataSource. Thường hay sử dụng PageKeyedDataSource hoặc ItemKeyedDataSource.
  • Database only: Sử dụng recyclerview với Room. Theo cách đó, bất cứ khi nào data được insert hay modified trong database, những thay đổi đó sẽ tự động cập nhật lên RecyclerView. Thường dùng với PositionalDataSource vì Room có thể tạo ra một Factory của PositionalDataSource.
  • Network & Database: Observable database, lắng nghe khi nào database hết dữ liệu bằng cách dùng PagedList.BoundaryCallback. Sau đó bạn có thể tải thêm dữ liệu từ network và insert vào database. Quan sát mô hình sau:

Ta chỉ quan sát DB và sử dụng nó làm source duy nhất, và ta chỉ tải dữ liệu chỉ trong một trường hợp duy nhất đó là khi DB báo hiệu hết data, khi này ta sẽ load thêm data từ network rồi lưu trữ vào DB và hiển thị. Như vậy, lợi ích mà nó đem lại là: hiển thị dữ liệu nhất quán, quá trình đơn giản - cần thêm thì load more. Sau khi bạn bắt đầu quan sát cơ sở dữ liệu, bạn có thể lắng nghe khi cơ sở dữ liệu hết dữ liệu bằng cách sử dụng PagedList.BoundaryCallback. Sau đó, bạn có thể tìm nạp thêm các item từ mạng của mình và chèn chúng vào cơ sở dữ liệu. Lúc này thì local device của bạn như là một bộ đệm.

Vậy tùy vào từng mô hình mà mọi người có cách áp dụng phù hợp!

Code demo: https://github.com/DongHien0896/Traning-Kotlin

Chúc mọi người code vui vẻ!

5. Tài liệu tham khảo

https://developer.android.com/reference/android/arch/paging/ItemKeyedDataSource

https://medium.com/@sharmadhiraj.np/android-paging-library-step-by-step-implementation-guide-75417753d9b9


All Rights Reserved