Tổng quan về thư viện Paging 2   Một phần của Android Jetpack.

Thư viện Paging giúp bạn tải và hiển thị đồng thời nhiều phần dữ liệu nhỏ. Việc tải một phần dữ liệu theo yêu cầu sẽ làm giảm mức sử dụng băng thông mạng và tài nguyên hệ thống.

Hướng dẫn này cung cấp một số ví dụ khái niệm cũng như tổng quan về cách hoạt động của thư viện. Để xem toàn bộ ví dụ về cách thư viện này hoạt động, hãy tham khảo lớp học lập trình và các mẫu trong phần tài nguyên bổ sung.

Thiết lập

Để nhập các thành phần Paging vào ứng dụng Android, hãy thêm các phần phụ thuộc sau vào tệp build.gradle của ứng dụng:

Groovy

dependencies {
  def paging_version = "2.1.2"

  implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
}

Kotlin

dependencies {
  val paging_version = "2.1.2"

  implementation("androidx.paging:paging-runtime:$paging_version") // For Kotlin use paging-runtime-ktx

  // alternatively - without Android dependencies for testing
  testImplementation("androidx.paging:paging-common:$paging_version") // For Kotlin use paging-common-ktx

  // optional - RxJava support
  implementation("androidx.paging:paging-rxjava2:$paging_version") // For Kotlin use paging-rxjava2-ktx
}

Cấu trúc thư viện

Phần này mô tả và cho thấy các thành phần chính của thư viện Paging.

PagedList

Thành phần chính của thư viện Paging là lớp PagedList, có nhiệm vụ tải các phần dữ liệu của ứng dụng hoặc các trang. Những dữ liệu cần thêm sẽ được phân trang vào đối tượng PagedList hiện có. Nếu có bất kỳ thay đổi nào về dữ liệu được tải thì một thực thể mới của PagedList sẽ được phát tới chủ sở hữu dữ liệu có thể quan sát từ LiveData hoặc đối tượng dựa trên RxJava2. Khi đối tượng PagedList được tạo, giao diện người dùng của ứng dụng sẽ trình bày nội dung của các mục đó mà vẫn tuân thủ vòng đời của bộ điều khiển giao diện người dùng.

Đoạn mã sau đây cho bạn biết cách định cấu hình mô hình xem của ứng dụng để tải và trình bày dữ liệu bằng cách dùng một chủ sở hữu LiveData của đối tượng PagedList:

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    // Creates a PagedList object with 50 items per page.
    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
                concertDao.concertsByDate(), 50).build();
    }
}

Dữ liệu

Mỗi thực thể của PagedList đều tải ảnh chụp nhanh mới nhất về dữ liệu của ứng dụng từ đối tượng DataSource tương ứng. Dữ liệu di chuyển từ phần phụ trợ hoặc cơ sở dữ liệu của ứng dụng tới đối tượng PagedList.

Ví dụ sau đây sử dụng Thư viện lưu trữ Room để sắp xếp dữ liệu của ứng dụng, nhưng nếu muốn lưu trữ dữ liệu bằng một cách khác thì bạn cũng có thể tự mình cung cấp nguồn dữ liệu nhà máy.

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a
    // PositionalDataSource object.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

Để tìm hiểu thêm về cách bạn có thể tải dữ liệu vào đối tượng PagedList, hãy xem hướng dẫn về cách Tải dữ liệu được phân trang.

Giao diện người dùng

Lớp PagedList hoạt động cùng PagedListAdapter để tải các mục vào một RecyclerView. Các lớp này cùng nhau tìm nạp, hiển thị nội dung khi được tải và tìm nạp trước các thay đổi về nội dung nằm ngoài chế độ xem cũng như nội dung tạo ảnh động.

Để tìm hiểu thêm, hãy xem hướng dẫn về cách Hiển thị danh sách đã phân trang.

Hỗ trợ nhiều cấu trúc dữ liệu

Thư viện Paging hỗ trợ các cấu trúc dữ liệu sau:

  • Chỉ được phân phát từ máy chủ phụ trợ.
  • Chỉ được lưu trữ trong cơ sở dữ liệu trên thiết bị.
  • Sự kết hợp của nhiều nguồn, dùng cơ sở dữ liệu trên thiết bị làm bộ nhớ đệm.

Hình 1 cho thấy cách dữ liệu di chuyển trong mỗi trường hợp cấu trúc này. Trong trường hợp giải pháp chỉ dành cho mạng hoặc cơ sở dữ liệu, dữ liệu sẽ chuyển thẳng đến mô hình giao diện của ứng dụng. Nếu bạn đang sử dụng phương pháp kết hợp thì dữ liệu sẽ chuyển từ máy chủ phụ trợ vào cơ sở dữ liệu trên thiết bị, sau đó chuyển đến mô hình giao diện của ứng dụng. Đôi khi, điểm cuối của mỗi luồng dữ liệu sẽ hết dữ liệu để tải. Lúc đó, thiết bị sẽ yêu cầu thêm dữ liệu từ thành phần đã cung cấp dữ liệu. Ví dụ: khi một cơ sở dữ liệu trên thiết bị hết dữ liệu, đơn vị này sẽ yêu cầu thêm dữ liệu từ máy chủ.

Sơ đồ về các luồng dữ liệu
Hình 1. Cách dữ liệu chuyển qua từng cấu trúc mà thư viện Paging hỗ trợ
.

Phần còn lại của mục này cung cấp những đề xuất để định cấu hình từng trường hợp sử dụng luồng dữ liệu.

Chỉ với mạng

Để hiển thị dữ liệu từ máy chủ phụ trợ, hãy sử dụng phiên bản đồng bộ của API Retrofit để tải thông tin vào đối tượng DataSource tuỳ chỉnh của bạn.

Chỉ với cơ sở dữ liệu

Hãy thiết lập RecyclerView để quan sát dữ liệu cục bộ và bạn nên thiết lập bằng Thư viện lưu trữ Room. Bằng cách đó, bất cứ khi nào dữ liệu được chèn hoặc sửa đổi trong cơ sở dữ liệu của ứng dụng, những thay đổi này sẽ tự động được phản ánh trong RecyclerView đang hiển thị dữ liệu này.

Mạng và cơ sở dữ liệu

Sau khi bắt đầu quan sát, bạn có thể biết khi nào 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 mục từ mạng của mình và chèn các mục đó vào cơ sở dữ liệu. Nếu giao diện người dùng đang quan sát cơ sở dữ liệu, thì đó là tất cả những gì bạn cần làm.

Xử lý lỗi mạng

Khi sử dụng mạng để tìm nạp hoặc phân trang dữ liệu bạn đang hiển thị bằng thư viện Paging, bạn không thể lúc nào cũng coi mạng là "có sẵn" hoặc "không có sẵn". Điều này rất quan trọng vì nhiều khi kết nối sẽ bị gián đoạn hoặc rời rạc:

  • Một máy chủ cụ thể có thể sẽ không phản hồi được yêu cầu kết nối mạng.
  • Có thể thiết bị kết nối với một mạng chậm hoặc yếu.

Thay vào đó, ứng dụng sẽ kiểm tra từng yêu cầu để tìm lỗi và khôi phục nhanh nhất có thể trong trường hợp không có mạng. Ví dụ: bạn có thể cung cấp nút "thử lại" để người dùng chọn nếu bước làm mới dữ liệu không hoạt động. Nếu xảy ra lỗi trong bước phân trang dữ liệu thì tốt nhất bạn nên thử tính năng tự động gửi lại các yêu cầu phân trang.

Cập nhật ứng dụng hiện có

Nếu ứng dụng của bạn đã sử dụng dữ liệu từ một cơ sở dữ liệu hoặc nguồn phụ trợ, thì bạn có thể nâng cấp trực tiếp lên chức năng mà thư viện Paging cung cấp. Phần này cho biết cách nâng cấp một ứng dụng có thiết kế có sẵn phổ biến.

Giải pháp phân trang tuỳ chỉnh

Nếu sử dụng chức năng tuỳ chỉnh để tải các tập hợp dữ liệu con từ nguồn dữ liệu của ứng dụng, bạn có thể thay thế logic này bằng logic của lớp PagedList. Các thực thể của PagedList cung cấp kết nối tích hợp sẵn cho các nguồn dữ liệu phổ biến. Những thực thể này cũng cung cấp bộ chuyển đổi cho các đối tượng RecyclerView mà bạn có thể đưa vào giao diện của ứng dụng.

Dữ liệu được tải bằng cách sử dụng danh sách thay vì trang

Nếu bạn sử dụng một danh sách trong bộ nhớ làm cấu trúc dữ liệu sao lưu cho bộ chuyển đổi của giao diện, hãy cân nhắc việc theo dõi các bản cập nhật dữ liệu bằng cách sử dụng lớp PagedList nếu số lượng mục trong danh sách có thể lớn. Các thực thể của PagedList có thể sử dụng LiveData<PagedList> hoặc Observable<List> để truyền các bản cập nhật dữ liệu đến giao diện của ứng dụng. Việc này sẽ giúp giảm thiểu thời gian tải và mức sử dụng bộ nhớ. Hơn thế, bạn có thể thay thế đối tượng List bằng đối tượng PagedList trong ứng dụng mà không phải thực hiện bất kỳ thay đổi nào trong cấu trúc giao diện người dùng hoặc logic cập nhật dữ liệu của ứng dụng.

Liên kết con trỏ dữ liệu với chế độ xem danh sách bằng CursorAdapter

Ứng dụng của bạn có thể sử dụng CursorAdapter để liên kết dữ liệu của Cursor với ListView. Trong trường hợp đó, thường thì bạn cần di chuyển vùng chứa từ ListView đến RecyclerView cho danh sách giao diện người dùng của ứng dụng, sau đó thay thế thành phần Cursor với Room hoặc PositionalDataSource. Việc chọn thành phần nào còn tuỳ thuộc vào việc các thực thể của Cursor có truy cập vào cơ sở dữ liệu SQLite hay không.

Trong một số trường hợp, chẳng hạn như khi làm việc với các thực thể của Spinner, bạn chỉ cần cung cấp chính bộ chuyển đổi đó. Sau đó, thư viện sẽ lấy dữ liệu đã được tải vào bộ chuyển đổi và cho bạn xem dữ liệu. Trong những trường hợp này, hãy thay đổi loại dữ liệu của bộ chuyển đổi thành LiveData<PagedList>, sau đó bọc danh sách này trong một đối tượng ArrayAdapter trước khi thử để lớp thư viện làm tăng các mục này trong giao diện người dùng.

Tải không đồng bộ nội dung bằng AsyncListUtil

Nếu bạn đang sử dụng các đối tượng AsyncListUtil để tải và hiển thị không đồng bộ nhóm thông tin, thì thư viện Paging sẽ giúp bạn tải dữ liệu dễ dàng hơn:

  • Dữ liệu của bạn không cần phải có vị trí. Bạn có thể sử dụng thư viện Paging để tải dữ liệu trực tiếp từ phần phụ trợ bằng cách sử dụng các khoá mà mạng cung cấp.
  • Dữ liệu của bạn có thể lớn đến mức vô cùng. Bằng cách sử dụng thư viện Paging, bạn có thể tải dữ liệu vào các trang cho đến khi không còn dữ liệu nào.
  • Bạn có thể quan sát dữ liệu dễ dàng hơn. Thư viện Paging có thể hiển thị dữ liệu mà ViewModel của ứng dụng chứa trong cấu trúc dữ liệu có thể quan sát.

Ví dụ về cơ sở dữ liệu

Các đoạn mã sau đây cho thấy một số cách để tất cả thành phần có thể hoạt động cùng nhau.

Quan sát dữ liệu đã phân trang bằng LiveData

Đoạn mã sau đây cho thấy tất cả thành phần đang hoạt động cùng nhau. Khi các sự kiện buổi hoà nhạc được thêm, xoá hoặc thay đổi trong cơ sở dữ liệu, nội dung trong RecyclerView sẽ được cập nhật tự động một cách hiệu quả:

Kotlin

@Dao
interface ConcertDao {
    // The Int type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    fun concertsByDate(): DataSource.Factory<Int, Concert>
}

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: LiveData<PagedList<Concert>> =
            concertDao.concertsByDate().toLiveData(pageSize = 50)
}

class ConcertActivity : AppCompatActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Use the 'by viewModels()' Kotlin property delegate
        // from the activity-ktx artifact
        val viewModel: ConcertViewModel by viewModels()
        val recyclerView = findViewById(R.id.concert_list)
        val adapter = ConcertAdapter()
        viewModel.concertList.observe(this, PagedList(adapter::submitList))
        recyclerView.setAdapter(adapter)
    }
}

class ConcertAdapter() :
        PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
        val concert: Concert? = getItem(position)

        // Note that "concert" is a placeholder if it's null.
        holder.bindTo(concert)
    }

    companion object {
        private val DIFF_CALLBACK = object :
                DiffUtil.ItemCallback<Concert>() {
            // Concert details may have changed if reloaded from the database,
            // but ID is fixed.
            override fun areItemsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert.id == newConcert.id

            override fun areContentsTheSame(oldConcert: Concert,
                    newConcert: Concert) = oldConcert == newConcert
        }
    }
}

Java

@Dao
public interface ConcertDao {
    // The Integer type parameter tells Room to use a PositionalDataSource
    // object, with position-based loading under the hood.
    @Query("SELECT * FROM concerts ORDER BY date DESC")
    DataSource.Factory<Integer, Concert> concertsByDate();
}

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final LiveData<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;
        concertList = new LivePagedListBuilder<>(
            concertDao.concertsByDate(), /* page size */ 50).build();
    }
}

public class ConcertActivity extends AppCompatActivity {
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ConcertViewModel viewModel =
                new ViewModelProvider(this).get(ConcertViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.concert_list);
        ConcertAdapter adapter = new ConcertAdapter();
        viewModel.concertList.observe(this, adapter::submitList);
        recyclerView.setAdapter(adapter);
    }
}

public class ConcertAdapter
        extends PagedListAdapter<Concert, ConcertViewHolder> {
    protected ConcertAdapter() {
        super(DIFF_CALLBACK);
    }

    @Override
    public void onBindViewHolder(@NonNull ConcertViewHolder holder,
            int position) {
        Concert concert = getItem(position);
        if (concert != null) {
            holder.bindTo(concert);
        } else {
            // Null defines a placeholder item - PagedListAdapter automatically
            // invalidates this row when the actual object is loaded from the
            // database.
            holder.clear();
        }
    }

    private static DiffUtil.ItemCallback<Concert> DIFF_CALLBACK =
            new DiffUtil.ItemCallback<Concert>() {
        // Concert details may have changed if reloaded from the database,
        // but ID is fixed.
        @Override
        public boolean areItemsTheSame(Concert oldConcert, Concert newConcert) {
            return oldConcert.getId() == newConcert.getId();
        }

        @Override
        public boolean areContentsTheSame(Concert oldConcert,
                Concert newConcert) {
            return oldConcert.equals(newConcert);
        }
    };
}

Quan sát dữ liệu phân trang bằng RxJava2

Nếu muốn sử dụng RxJava2 thay vì LiveData, bạn có thể tạo đối tượng Observable hoặc Flowable:

Kotlin

class ConcertViewModel(concertDao: ConcertDao) : ViewModel() {
    val concertList: Observable<PagedList<Concert>> =
            concertDao.concertsByDate().toObservable(pageSize = 50)
}

Java

public class ConcertViewModel extends ViewModel {
    private ConcertDao concertDao;
    public final Observable<PagedList<Concert>> concertList;

    public ConcertViewModel(ConcertDao concertDao) {
        this.concertDao = concertDao;

        concertList = new RxPagedListBuilder<>(
                concertDao.concertsByDate(), /* page size */ 50)
                        .buildObservable();
    }
}

Sau đó, bạn có thể bắt đầu và dừng quan sát dữ liệu bằng cách sử dụng đoạn mã sau:

Kotlin

class ConcertActivity : AppCompatActivity() {
    private val adapter = ConcertAdapter()

    // Use the 'by viewModels()' Kotlin property delegate
    // from the activity-ktx artifact
    private val viewModel: ConcertViewModel by viewModels()

    private val disposable = CompositeDisposable()

    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val recyclerView = findViewById(R.id.concert_list)
        recyclerView.setAdapter(adapter)
    }

    override fun onStart() {
        super.onStart()
        disposable.add(viewModel.concertList
                .subscribe(adapter::submitList)))
    }

    override fun onStop() {
        super.onStop()
        disposable.clear()
    }
}

Java

public class ConcertActivity extends AppCompatActivity {
    private ConcertAdapter adapter = new ConcertAdapter();
    private ConcertViewModel viewModel;

    private CompositeDisposable disposable = new CompositeDisposable();

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        RecyclerView recyclerView = findViewById(R.id.concert_list);

        viewModel = new ViewModelProvider(this).get(ConcertViewModel.class);
        recyclerView.setAdapter(adapter);
    }

    @Override
    protected void onStart() {
        super.onStart();
        disposable.add(viewModel.concertList
                .subscribe(adapter.submitList(flowableList)
        ));
    }

    @Override
    protected void onStop() {
        super.onStop();
        disposable.clear();
    }
}

Mã cho ConcertDaoConcertAdapter là giống nhau đối với giải pháp dựa trên RxJava2 vì chúng là để dành cho giải pháp dựa trên LiveData.

Gửi phản hồi

Hãy chia sẻ phản hồi và ý kiến của bạn với chúng tôi thông qua các tài nguyên sau:

Công cụ theo dõi lỗi
Báo cáo sự cố để chúng tôi có thể sửa lỗi.

Tài nguyên khác

Để tìm hiểu thêm về Thư viện Paging, hãy tham khảo các tài nguyên sau.

Mẫu

Lớp học lập trình

Video