ページング 2 ライブラリの概要 Android Jetpack の一部。

ページング ライブラリを使用すると、データの小さなチャンクを一度に読み込んで表示することができます。 部分的なデータをオンデマンドで読み込むことで、ネットワーク帯域幅とシステム リソースの使用量を削減できます。

このガイドでは、ページング ライブラリのコンセプトを例示し、ライブラリの仕組みについて概説します。このライブラリの機能の完全な例については、参考情報のコードラボとサンプルをご覧ください。

セットアップ

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

ページング ライブラリの重要なコンポーネントである PagedList クラスは、アプリのデータのチャンク(ページ)を読み込みます。必要なデータが増えると、既存の PagedList オブジェクトにデータがページングされます。読み込み済みのデータが変更されると、PagedList の新しいインスタンスが LiveData または RxJava2 ベースのオブジェクトから監視可能なデータホルダーに出力されます。PagedList オブジェクトが生成されると、UI コントローラのライフサイクルを考慮しながら、アプリの UI にオブジェクトの内容が表示されます。

次のコード スニペットは、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 オブジェクトに読み込む方法について詳しくは、ページング データを読み込む方法についてのガイドをご覧ください。

UI

PagedList クラスは PagedListAdapter と連携してアイテムを RecyclerView に読み込みます。これらのクラスは連携して機能し、コンテンツが読み込まれるとそれを取得して表示します。さらに、表示範囲外のコンテンツを事前に取得し、コンテンツの変更をアニメーション化します。

詳しくは、ページング リストを表示する方法についてのガイドをご覧ください。

各種のデータ アーキテクチャをサポート

ページング ライブラリは以下のデータ アーキテクチャをサポートしています。

  • 提供元がバックエンド サーバーのみのデータ アーキテクチャ。
  • 保存先がデバイス上のデータベースのみのデータ アーキテクチャ。
  • その他のソースの組み合わせ(デバイス上のデータベースをキャッシュとして使用)。

図 1 に、各アーキテクチャのシナリオでのデータフローを示します。ネットワークのみまたはデータベースのみのソリューションの場合、データはアプリの UI モデルに直接渡されます。複合的なアプローチでは、データはバックエンド サーバーからデバイス上のデータベースに渡され、その後でアプリの UI モデルに渡されます。ときどき、各データフローのエンドポイントで読み込むデータがなくなることがあり、その場合はデータの提供元のコンポーネントに追加のデータをリクエストします。たとえば、デバイス上のデータベースでデータがなくなった場合、サーバーに追加のデータをリクエストします。

データフローの図
図 1. ページング ライブラリがサポートする各アーキテクチャでのデータフロー

ここからは、各データフローのユースケースの設定に関する推奨事項を紹介します。

ネットワークのみ

バックエンド サーバーのデータを表示するには、Retrofit API の同期バージョンを使用して、情報を独自のカスタム DataSource オブジェクトに読み込みます。

データベースのみ

ローカル ストレージを監視するように RecyclerView を設定します(Room 永続ライブラリを使用することをおすすめします)。これにより、アプリのデータベース内でデータの挿入または変更が行われると、そのデータを表示する RecyclerView に変更内容が自動的に反映されます。

ネットワークとデータベース

データベースの監視を開始すると、PagedList.BoundaryCallback を使用してデータベースのデータが不足するタイミングをリッスンできます。その後、ネットワークから追加のアイテムを取得してデータベースに挿入することができます。UI でデータベースを監視している場合に必要な操作はこれだけです。

ネットワーク エラーに対処する

ページング ライブラリを使用して表示するデータをネットワーク経由で取得またはページングする場合、多くの接続は断続的または不安定であるため、ネットワークを常に「使用可」または「使用不可」として扱わないようにすることが重要です。

  • 特定のサーバーがネットワーク リクエストに応答できない可能性があります。
  • デバイスが低速のネットワークや電波の弱いネットワークに接続されている可能性があります。

代わりに、アプリでエラーに対する要求を 1 つずつチェックして、ネットワークが使用不可の場合はできる限りスムーズに復元する必要があります。たとえば、データの更新操作が機能しない場合、ユーザーが選択する「再試行」ボタンを表示できます。データのページング中にエラーが発生した場合は、ページング リクエストを自動的に再試行することをおすすめします。

既存のアプリを更新する

データベースまたはバックエンド ソースのデータをアプリですでに使用している場合は、ページング ライブラリが提供する機能に直接アップグレードできます。このセクションでは、既存の一般的な設計のアプリをアップグレードする方法について説明します。

カスタムのページング ソリューション

カスタム機能を使用してアプリのデータソースからデータの一部を読み込んでいる場合は、このロジックを PagedList クラスのロジックに置き換えることができます。PagedList のインスタンスには、共通のデータソースへの接続機能が組み込まれています。また、アプリの UI に含めることができる RecyclerView オブジェクト用のアダプターも用意されています。

ページではなくリストを使用して読み込まれたデータ

メモリ内リストを UI のアダプターのバッキング データ構造として使用する場合、リスト内のアイテム数が多くなる可能性があるときには、PagedList クラスを使用してデータの更新を監視することを検討してください。PagedList のインスタンスでは LiveData<PagedList> または Observable<List> を使用してデータの更新をアプリの UI に渡すことができ、その結果、読み込み時間とメモリ使用量を最小限に抑えることができます。さらに、アプリ内で List オブジェクトを PagedList オブジェクトに置き換えることで、アプリの UI 構造やデータ更新ロジックの変更が不要になります。

CursorAdapter を使用してデータカーソルをリストビューに関連付ける

アプリでは、CursorAdapter を使用して Cursor のデータを ListView に関連付ける場合があります。その場合は通常、アプリのリスト UI コンテナとして ListView から RecyclerView に移行する必要があります。さらに、Cursor のインスタンスが SQLite データベースにアクセスするかどうかに応じて、Cursor コンポーネントを Room または PositionalDataSource に置き換えます。

Spinner のインスタンスを使用する場合などは、アダプター自体のみを指定します。そうすることで、ライブラリがそのアダプターに読み込まれたデータを取得して表示します。このような場合は、アダプターのデータ型を LiveData<PagedList> に変更し、そのリストを ArrayAdapter オブジェクトにラップします。ライブラリ クラスによる UI のアイテムのインフレーションはこの後で行います。

AsyncListUtil を使用してコンテンツを非同期で読み込む

AsyncListUtil オブジェクトを使用して情報グループの読み込みと表示を非同期で行っている場合は、ページング ライブラリを使用することで、データをより簡単に読み込めるようになります。

  • データの位置を指定する必要はありません。ページング ライブラリを使用すると、ネットワークから提供されるキーを使用して、バックエンドからデータを直接読み込むことができます。
  • データは膨大な量になる可能性があります。ページング ライブラリを使用すると、残りのデータがなくなるまでデータをページに読み込むことができます。
  • データをより簡単に監視できます。ページング ライブラリを使用すると、アプリの 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 によるページング データの監視

LiveData ではなく RxJava2 を使用する場合は、Observable または 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();
    }
}

次のスニペットのコードを使用すると、データの監視を開始および停止できます。

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

ConcertDaoConcertAdapter のコードは、RxJava2 ベースのソリューションのものと同一です(これらのコードは LiveData ベースのソリューションを対象としています)。

フィードバックを送信

以下のリソースを通じてフィードバックやアイデアをお寄せください。

Issue Tracker
Google がバグを修正できるよう問題を報告します。

参考情報

ページング ライブラリについて詳しくは、以下のリソースをご覧ください。

サンプル

Codelab

動画