Android
architecture-components

【Android Architecture Components】Paging Library 和訳

More than 1 year has passed since last update.

Android Architecture Componentsの1.0.0 Alpha 9-1から新しく追加されたPagingライブラリのドキュメントを和訳したものになります。

Pagingライブラリはデバイスに過負荷をかけることなく、またはDBにリクエストをするために長時間待機されることなく、データソースから徐々にデータを取得する事を容易にしてくれます。

概要

多くのアプリは多くのデータを使用して動いていますが、画面を表示するのに必要な情報はその一部の小さなデータです。
アプリは何千ものアイテムを表示するかもしれませんが、1度のアクセスでは纏まった少量のデータだけ必要かもしれません。
もし考慮が足りない場合には実際に必要のないデータまで要求してしまう事になり、デバイスとネットワークのパフォーマンスに負担がかかります。
データがリモートDBに保存または同期されている場合は、アプリの処理速度が遅くなり、ユーザーの通信が無駄に浪費される可能性があります。

既存のAndroid APIはコンテンツのページングを許可していましたが、大きな制約と欠点がありました。

  • CursorAdapterを使用すると、データベースのクエリ結果をListViewアイテムに簡単にマッピングできます。しかし、データベースのクエリはUIスレッド上で実行され、Cursorで非効率にページコンテンツを表示します。CursorAdapterの使用上の不明点はブログに投稿されたLarge Database Queries on Androidをみてください。
  • AsyncListUtilは位置ベースのデータをRecyclerViewにページングすることを可能にしますが、非ページングを許可せず、カウント可能なデータセットにプレースホルダとしてのNULLを強制します。

新しいページングライブラリは、これらの問題に対処します。
このライブラリには、必要なときにデータを要求するプロセスを合理化するためのいくつかのクラスが含まれています。
これらのクラスは、Roomなどの既存のアーキテクチャコンポーネントとシームレスに連携します。

クラス

Pagingライブラリには、次のクラスと追加のサポートクラスが用意されています。

DataSource

このクラスを使用して、取得する必要があるページデータのデータソースを定義します。データへのアクセス方法に応じて、2つのサブクラスのうち1つを継承します。

  • アイテムNのデータを使用してアイテムN + 1をフェッチする必要がある場合は、KeyedDataSourceを使用します。たとえば、ディスカッションアプリのスレッドコメントを取得する場合は、次のコメントの内容を取得するために1つのコメントのIDを渡す必要があります。
  • データストアで選択した任意の場所からページのデータを取得する必要がある場合は、TiledDataSourceを使用します。このクラスは、「1200番目から20個のデータを返す」のように、指定した場所からデータのセットを要求することをサポートしています。

もし、Roomライブラリでデータ管理をしている場合は、TiledDataSourceを自動的に生成することができます。

@Query("select * from users WHERE age > :age order by name DESC, id ASC")
TiledDataSource<User> usersOlderThan(int age);

PagedList

このクラスはDataSourceからデータをロードします。あなたは一回の読み込みでどのくらいデータを読み込むか、どのくらいのデータを先読みするかを設定し、ユーザーがデータのロードを待つ時間を最小限に抑えます。
このクラスは、RecyclerView.Adapterなどの他のクラスに更新を知らせることができ、データがページにロードされるときにRecyclerViewの内容を更新できるようにします。

PagedListAdapter

このクラスは、PagedListからデータを渡すためにRecyclerView.Adapterを実装したクラスです。
例えば新しいページをロードした時、PagedListAdapterRecyclerViewにデータが来た事を知らせます。
これにより、RecyclerViewはプレースホルダを実際のアイテムに置き換え、適切なアニメーションを実行できます。
また、PagedListAdapterはバックグラウンドスレッドを使用して、あるPagedListから次のPagedListへの変更を計算して(たとえば、データベースの変更によって更新されたデータで新しいPagedListが生成された場合など)、必要に応じてnotifyItem...()メソッドを呼び出してリストの内容を更新します。
その後、RecyclerViewは必要な変更を実行します。たとえば、アイテムがPagedListバージョン間で位置が変更した場合、RecyclerViewは、そのアイテムをリスト内の新しい場所にアニメーションして移動します。

LivePagedListProvider

このクラスはあなたが提供するDataSourceからLiveData<PagedList>を生成します。さらに、DBの管理にRoomライブラリを使用している場合、DAOはTiledDataSourceを使用してLivePagedListProviderを生成できます。たとえば、次のようになります。

@Query("SELECT * from users order WHERE age > :age order by name DESC, id ASC")
public abstract LivePagedListProvider<Integer, User> usersOlderThan(int age);

Integer パラメータは、Roomに、位置ベースの読み込みでTiledDataSourceを使用するように指示します。

同時に、Pagingライブラリのコンポーネントは、バックグラウンドスレッドからUIスレッドへのデータフローを構成します。たとえば、新しい項目がDBに挿入されると、DataSourceは無効になり、LivePagedListProviderはバックグラウンドスレッドで新しいPagedListを生成します。

paging-threading.gif

新しく生成されたPagedListはUIスレッド上でPagedListAdapterに送られます。
PagedListAdapterはバックグラウンドでDiffUtilを使って新しいリストと現在のリストの差分を計算します。比較が終わったらPagedListAdapterはリストの差分情報を使用してRecyclerView.Adapter.notifyItemInserted()を適切に呼び出して、新しい項目が挿入されたことを知らせます。

UIスレッドのRecyclerViewは一つの新しい項目をアニメーションさせながらバインドすることだけを知ります。

次のサンプルコードは、これらのクラスを使った一連のコードを示しています。
ユーザーがデータベースに追加、削除、変更されると、RecyclerViewのコンテンツは自動的かつ効率的に更新されます。

@Dao
interface UserDao {
    @Query("SELECT * FROM user ORDER BY lastName ASC")
    public abstract LivePagedListProvider<Integer, User> usersByLastName();
}

class MyViewModel extends ViewModel {
    public final LiveData<PagedList<User>> usersList;
    public MyViewModel(UserDao userDao) {
        usersList = userDao.usersByLastName().create(
                /* initial load position */ 0,
                new PagedList.Config.Builder()
                        .setPageSize(50)
                        .setPrefetchDistance(50)
                        .build());
    }
}

class MyActivity extends AppCompatActivity {
    @Override
    public void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        UserAdapter<User> adapter = new UserAdapter();
        viewModel.usersList.observe(this, pagedList -> adapter.setList(pagedList));
        recyclerView.setAdapter(adapter);
    }
}

class UserAdapter extends PagedListAdapter<User, UserViewHolder> {
    public UserAdapter() {
        super(DIFF_CALLBACK);
    }
    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = getItem(position);
        if (user != null) {
            holder.bindTo(user);
        } else {
            // Null defines a placeholder item - PagedListAdapter will automatically invalidate
            // this row when the actual object is loaded from the database
            holder.clear();
        }
    }
    public static final DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
        @Override
        public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // User properties may have changed if reloaded from the DB, but ID is fixed
            return oldUser.getId() == newUser.getId();
        }
        @Override
        public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
            // NOTE: if you use equals, your object must properly override Object#equals()
            // Incorrectly returning false here will result in too many animations.
            return oldUser.equals(newUser);
        }
    }
}