Paging 2 程式庫總覽 Android Jetpack 的一部分。

Paging Library 可讓您一次載入及顯示小區塊的資料。視需求載入部分資料會減少網路頻寬和系統資源用量。

本指南提供多個程式庫的概念範例,並概略說明其運作方式。如要查看這個程式庫功能的完整範例,請前往其他資源一節查看程式碼研究室和範例。

設定

如要將 Paging 元件匯入 Android 應用程式,請在應用程式的 build.gradle 檔案中新增下列依附元件:

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
}

程式庫架構

本節將說明並顯示分頁程式庫的主要元件。

PagedList

Paging Library 的關鍵元件為 PagedList 類別,該類別會載入應用程式資料區塊或「資訊頁面」。需要更多資料時,系統會將資料分頁至現有的 PagedList 物件。如果載入的資料有所變更,系統會從 LiveData 或 RxJava2 物件傳送新的 PagedList 執行個體至可觀測的資料持有人。產生 PagedList 物件時,應用程式的使用者介面會顯示完全遵守使用者介面控制器生命週期的內容。

下列程式碼片段說明如何設定應用程式的檢視模型,以使用 PagedList 物件的 LiveData 持有人來載入及呈現資料:

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();
    }
}

資料

每個 PagedList 執行個體都會從對應的 DataSource 物件載入應用程式資料的最新快照。資料從應用程式的後端或資料庫流入 PagedList 物件。

以下範例使用 Room 持續性程式庫整理應用程式資料,但如果您想透過其他方式儲存資料,也可以選擇提供原廠資料來源

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();
}

如要進一步瞭解如何將資料載入 PagedList 物件,請參閱「載入分頁資料」指南。

使用者介面

PagedList 類別可搭配 PagedListAdapter 使用,將項目載入 RecyclerView。這些類別會共同運行以擷取和顯示內容,作為載入資料、預先擷取檢視以外的內容以及動畫內容變更

詳情請參閱如何「顯示分頁清單」指南。

支援不同的資料架構

Paging Library 支援下列資料架構:

  • 僅從後端伺服器提供。
  • 僅儲存在裝置端的資料庫。
  • 使用裝置端資料庫做為快取的其他來源組合。

圖 1 顯示了每個架構情境中的資料流動方式。針對僅限網路或僅限資料庫的解決方案,資料會直接流向應用程式的使用者介面模型。如果您使用組合方法,資料從後端伺服器流入裝置端資料庫,再導向應用程式的使用者介面模型。每個資料流的端點會在一段時間內用盡,以將資料載入,此時便會向提供資料的元件要求更多資料。舉例來說,當裝置端的資料庫耗盡資料時,就會向伺服器要求更多資料。

資料流程圖
Figure 1.Paging Library 與不同架構之間的資料流動方式

本節的其餘部分會針對設定各個資料流用途提供建議。

僅限網路

如要顯示後端伺服器的資料,請使用同步版的 Retrofit API,將資訊載入自訂的 DataSource 物件

僅限資料庫

設定 RecyclerView 來觀察本機儲存空間,最好使用 Room 持續性程式庫。這樣一來,當您的應用程式資料庫中插入或修改資料時,這些變更就會自動反映在顯示這項資料的 RecyclerView 中。

網路和資料庫

開始觀測資料庫後,您可以使用 PagedList.BoundaryCallback 監聽資料庫何時不在資料範圍內。接著,您可以從網路擷取更多項目,然後將其插入資料庫。如果您的使用者介面觀測到資料庫,這就是您需要執行的所有動作。

處理網路錯誤

使用網路擷取或分頁使用 Paging Library 顯示的資料時,建議您不要將網路視為「可使用」或「無法使用」,因為許多連線是斷斷續續或不穩定的。

  • 特定伺服器可能無法回應網路要求。
  • 裝置可能連接至速度緩慢或微弱的網路。

而是應用程式應檢查每個要求是否失敗,並在無法使用網路時盡可能復原。舉例來說,如果資料重新整理步驟無法運作,您可以提供「重試」按鈕讓使用者進行選取。如果資料分頁步驟發生錯誤,建議您自動重試分頁要求。

更新現有應用程式

如果您的應用程式已採用資料庫或後端來源的資料,您可以直接升級至 Paging Library 提供的功能。本節說明如何升級現有通用設計的應用程式。

自訂分頁解決方案

如果您利用自訂功能從應用程式的資料來源載入一小部分資料,您可以將這個邏輯替換為 PagedList 類別的邏輯。PagedList 的執行個體提供常見資料來源的內建連線。這些執行個體也會提供 RecyclerView 物件轉接程式,供您加入應用程式使用者介面。

使用清單 (而非分頁) 載入資料

如果您使用記憶體內清單做為使用者介面轉接程式的備份資料結構,請考慮使用 PagedList 類別觀測資料更新 (如果清單中的項目數量可以變大)。PagedList 的執行個體可使用 LiveData<PagedList>Observable<List> 將資料更新傳送至應用程式的使用者介面,以盡量縮短載入時間和記憶體用量。更理想的是,將 List 物件替換為應用程式 PagedList 物件,無須變更應用程式的使用者介面結構或資料更新邏輯。

使用 CursorAdapter 將資料游標與清單檢視畫面建立關聯

您的應用程式可能會使用 CursorAdapter,將來自 Cursor 的資料與 ListView 建立關聯。在這種情況下,您通常必須從 ListView 遷移至 RecyclerView 做為應用程式的清單使用者介面容器,然後將 Cursor 元件更換為 RoomPositionalDataSource,取決於 Cursor 執行個體是否存取 SQLite 資料庫。

在某些情況下 (例如使用 Spinner 執行個體時),您只需要提供轉接程式。接著,程式庫會使用已載入至該轉接程式的資料,並顯示資料。在這些情況下,請將轉接程式資料類型變更為 LiveData<PagedList>,然後將這個清單包裝至 ArrayAdapter 物件,否則物件會嘗試使用程式庫類別在使用者介面中加載這些項目。

使用 AsyncListUtil 以非同步方式載入內容

如果您使用 AsyncListUtil 物件以非同步方式載入及顯示資訊群組,Paging Library 可讓您更輕鬆地載入資料:

  • 資料無須定位。Paging Library 可讓您利用網路提供的金鑰,直接從後端載入資料。
  • 您的資料可能相當龐大。透過 Paging Library,您可以將資料載入至頁面,直到沒有任何剩餘資料為止。
  • 更輕鬆地觀測資料。Paging 程式庫在可觀測的資料結構中,呈現您應用程式的 ViewModel 所保存的資料。

資料庫範例

下列程式碼片段顯示讓不同片段互相搭配運作的方式。

使用 LiveData 觀察分頁資料

下列程式碼片段會顯示所有能搭配運作的部分。當資料庫中新增音樂活動、移除或變更演唱會事件時,會自動且有效率地更新 RecyclerView 的內容:

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);
        }
    };
}

使用 RxJava2 觀察分頁資料

如果您偏好使用 RxJava2 取代 LiveData,可以改為建立 ObservableFlowable 物件:

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();
    }
}

接著,您可以使用下列程式碼片段中的程式碼來啟動及停止觀察中的資料:

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();
    }
}

RxJava2 為基礎的解決方中 ConcertDaoConcertAdapter 的程式碼與以 LiveData 為基礎的解決方案中的程式碼相同。

提供意見

歡迎透過下列資源與我們分享意見和想法:

Issue Tracker
報告問題,幫助我們修正錯誤。

其他資源

如要進一步瞭解 Paging Library,請參閱下列資源。

範例

程式碼研究室

影片