ページング ライブラリには、大規模なデータセットからページング データを読み込み、表示する強力な機能があります。このガイドでは、ページング ライブラリを使用し、ネットワーク データソースからのページング データのストリームを設定して RecyclerView
で表示する方法について説明します。
データソースを定義する
最初のステップは、データソースを識別する PagingSource
実装を定義することです。PagingSource
API クラスには load()
メソッドが含まれています。このメソッドは、対応するデータソースからページング データを取得する方法を示すためにオーバーライドする必要があります。
非同期読み込みに Kotlin コルーチンを使用するには、PagingSource
クラスを直接使用します。ページング ライブラリには、他の非同期フレームワークをサポートするクラスも用意されています。
- RxJava を使用する場合、代わりに
RxPagingSource
を実装します。 - Guava の
ListenableFuture
を使用する場合、代わりにListenableFuturePagingSource
を実装します。
キーと値の型を選択する
PagingSource<Key, Value>
には、Key
と Value
の 2 種類のパラメータがあります。キーは、データの読み込みに使用される識別子を定義します。値は、データ自体の型になります。たとえば、Int
ページ番号を Retrofit に渡してネットワークから User
オブジェクトのページを読み込む場合は、Key
型として Int
を、Value
型として User
を選択します。
PagingSource を定義する
次の例では、ページ番号ごとにアイテムのページを読み込む PagingSource
を実装します。Key
型は Int
、Value
型は User
です。
Kotlin
class ExamplePagingSource( val backend: ExampleBackendService, val query: String ) : PagingSource<Int, User>() { override suspend fun load( params: LoadParams<Int> ): LoadResult<Int, User> { try { // Start refresh at page 1 if undefined. val nextPageNumber = params.key ?: 1 val response = backend.searchUsers(query, nextPageNumber) return LoadResult.Page( data = response.users, prevKey = null, // Only paging forward. nextKey = response.nextPageNumber ) } catch (e: Exception) { // Handle errors in this block and return LoadResult.Error if it is an // expected error (such as a network failure). } } }
Java
class ExamplePagingSource extends RxPagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; ExamplePagingSource(@NonNull ExampleBackendService backend, @NonNull String query) { mBackend = backend; mQuery = query; } @NotNull @Override public Single<LoadResult<Integer, User>> loadSingle( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } return mBackend.searchUsers(mQuery, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUsersResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } }
Java
class ExamplePagingSource extends ListenableFuturePagingSource<Integer, User> { @NonNull private ExampleBackendService mBackend; @NonNull private String mQuery; @NonNull private Executor mBgExecutor; ExamplePagingSource( @NonNull ExampleBackendService backend, @NonNull String query, @NonNull Executor bgExecutor) { mBackend = backend; mQuery = query; mBgExecutor = bgExecutor; } @NotNull @Override public ListenableFuture<LoadResult<Integer, User>> loadFuture( @NotNull LoadParams<Integer> params) { // Start refresh at page 1 if undefined. Integer nextPageNumber = params.getKey(); if (nextPageNumber == null) { nextPageNumber = 1; } ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( mBackend.searchUsers(mQuery, nextPageNumber), this::toLoadResult, mBgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, mBgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, mBgExecutor); } private LoadResult<Integer, User> toLoadResult( @NonNull SearchUsersResponse response) { return new LoadResult.Page<>( response.getUsers(), null, // Only paging forward. response.getNextPageNumber(), LoadResult.Page.COUNT_UNDEFINED, LoadResult.Page.COUNT_UNDEFINED); } }
一般的な PagingSource
実装では、コンストラクタで提供されたパラメータを load()
メソッドに渡して、クエリに適切なデータを読み込みます。上記の例では、これらのパラメータは次のとおりです。
backend
: データを提供するバックエンド サービスのインスタンス。query
:backend
で示されるサービスに送信する検索クエリ。
LoadParams
オブジェクトには、実行する読み込みオペレーションに関する情報が含まれます。これには、読み込むキーと、読み込むアイテムの数が含まれます。
LoadResult
オブジェクトには、読み込みオペレーションの結果が含まれます。LoadResult
は、load()
の呼び出しが成功したかどうかに応じて、次の 2 つの形式のいずれかになるシールクラスです。
- 読み込みが正常に完了したら、
LoadResult.Page
オブジェクトが返されます。 - 読み込みが成功しなかった場合は、
LoadResult.Error
オブジェクトが返されます。
図 1 は、この例の load()
関数が各読み込みのキーを受け取り、その後の読み込みのキーを提供する方法を示しています。
load()
がキーをどのように使用および更新するかを示す図。エラーを処理する
特にネットワーク経由でデータを読み込む場合、読み込みリクエストがさまざまな原因で失敗することがあります。load()
メソッドから LoadResult.Error
オブジェクトを返すことにより、読み込み中に発生したエラーを報告できます。
たとえば、load()
メソッドに以下を追加することで、前の例の ExamplePagingSource
での読み込みエラーをキャッチして報告できます。
Kotlin
catch (e: IOException) { // IOException for network failures. return LoadResult.Error(e) } catch (e: HttpException) { // HttpException for any non-2xx HTTP status codes. return LoadResult.Error(e) }
Java
return backend.searchUsers(searchTerm, nextPageNumber) .subscribeOn(Schedulers.io()) .map(this::toLoadResult) .onErrorReturn(LoadResult.Error::new);
Java
ListenableFuture<LoadResult<Integer, User>> pageFuture = Futures.transform( backend.searchUsers(query, nextPageNumber), this::toLoadResult, bgExecutor); ListenableFuture<LoadResult<Integer, User>> partialLoadResultFuture = Futures.catching( pageFuture, HttpException.class, LoadResult.Error::new, bgExecutor); return Futures.catching(partialLoadResultFuture, IOException.class, LoadResult.Error::new, bgExecutor);
Retrofit エラーの処理の詳細については、PagingSource
API リファレンスのサンプルをご覧ください。
PagingSource
は、LoadResult.Error
オブジェクトを収集して UI に配信し、ユーザーがこれを操作できるようにします。UI で読み込み状態を表示する方法について詳しくは、読み込み状態の表示をご覧ください。
PagingData のストリームを設定する
次に、PagingSource
の実装からページング データのストリームが必要になります。通常は、ViewModel
でデータ ストリームを設定する必要があります。Pager
クラスには、PagingData
オブジェクトの PagingSource
からのリアクティブ ストリームを表示するメソッドが用意されています。ページング ライブラリは、Flow
、LiveData
、RxJava の Flowable
や Observable
タイプなど、複数のストリーム タイプの使用をサポートしています。
リアクティブ ストリームをセットアップする Pager
インスタンスを作成する場合は、PagingConfig
構成オブジェクトと、PagingSource
実装のインスタンスを取得する方法を Pager
に指示する関数をインスタンスに提供する必要があります。
Kotlin
val flow = Pager( // Configure how data is loaded by passing additional properties to // PagingConfig, such as prefetchDistance. PagingConfig(pageSize = 20) ) { ExamplePagingSource(backend, query) }.flow .cachedIn(viewModelScope)
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); Flowable<PagingData<User>> flowable = PagingRx.getFlowable(pager); PagingRx.cachedIn(flowable, viewModelScope);
Java
// CoroutineScope helper provided by the lifecycle-viewmodel-ktx artifact. CoroutineScope viewModelScope = ViewModelKt.getViewModelScope(viewModel); Pager<Integer, User> pager = Pager<>( new PagingConfig(/* pageSize = */ 20), () -> ExamplePagingSource(backend, query)); PagingLiveData.cachedIn(PagingLiveData.getLiveData(pager), viewModelScope);
cachedIn()
演算子はデータ ストリームを共有可能にするとともに、指定された CoroutineScope
で読み込まれたデータをキャッシュに保存します。この例では、Lifecycle lifecycle-viewmodel-ktx
アーティファクトにより提供される viewModelScope
を使用しています。
Pager
オブジェクトは、PagingSource
オブジェクトから load()
メソッドを呼び出し、LoadParams
オブジェクトを提供し、それに応じて LoadResult
オブジェクトを受け取ります。
RecyclerView アダプターを定義する
RecyclerView
リストにデータを受信するようにアダプターを設定する必要があります。ページング ライブラリには、これを行うための PagingDataAdapter
クラスが用意されています。
PagingDataAdapter
を拡張するクラスを定義します。この例では、UserAdapter
が PagingDataAdapter
を拡張し、タイプ User
のリストアイテムに RecyclerView
アダプターを提供し、UserViewHolder
をビューホルダーとして使用します。
Kotlin
class UserAdapter(diffCallback: DiffUtil.ItemCallback<User>) : PagingDataAdapter<User, UserViewHolder>(diffCallback) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int ): UserViewHolder { return UserViewHolder(parent) } override fun onBindViewHolder(holder: UserViewHolder, position: Int) { val item = getItem(position) // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item) } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
Java
class UserAdapter extends PagingDataAdapter<User, UserViewHolder> { UserAdapter(@NotNull DiffUtil.ItemCallback<User> diffCallback) { super(diffCallback); } @NonNull @Override public UserViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new UserViewHolder(parent); } @Override public void onBindViewHolder(@NonNull UserViewHolder holder, int position) { User item = getItem(position); // Note that item may be null. ViewHolder must support binding a // null item as a placeholder. holder.bind(item); } }
アダプターで、onCreateViewHolder()
メソッドと onBindViewHolder()
メソッドを定義し、DiffUtil.ItemCallback
を指定します。これは、RecyclerView
リスト アダプターを定義するときと同じように機能します。
Kotlin
object UserComparator : DiffUtil.ItemCallback<User>() { override fun areItemsTheSame(oldItem: User, newItem: User): Boolean { // Id is unique. return oldItem.id == newItem.id } override fun areContentsTheSame(oldItem: User, newItem: User): Boolean { return oldItem == newItem } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
Java
class UserComparator extends DiffUtil.ItemCallback<User> { @Override public boolean areItemsTheSame(@NonNull User oldItem, @NonNull User newItem) { // Id is unique. return oldItem.id.equals(newItem.id); } @Override public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) { return oldItem.equals(newItem); } }
UI でページング データを表示する
PagingSource
を定義し、アプリで PagingData
のストリームを生成する手段を構築し、PagingDataAdapter
を定義したので、これらの要素を接続し、アクティビティにページング データを表示できるようになりました。
アクティビティの onCreate
またはフラグメントの onViewCreated
メソッドで次の手順を行います。
PagingDataAdapter
クラスのインスタンスを作成します。- ページング データを表示する
RecyclerView
リストにPagingDataAdapter
インスタンスを渡します。 PagingData
ストリームを監視し、生成されたそれぞれの値をアダプターのsubmitData()
メソッドに渡します。
Kotlin
val viewModel by viewModels<ExampleViewModel>() val pagingAdapter = UserAdapter(UserComparator) val recyclerView = findViewById<RecyclerView>(R.id.recycler_view) recyclerView.adapter = pagingAdapter // Activities can use lifecycleScope directly, but Fragments should instead use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { viewModel.flow.collectLatest { pagingData -> pagingAdapter.submitData(pagingData) } }
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter viewModel.flowable // Using AutoDispose to handle subscription lifecycle. // See: https://github.com/uber/AutoDispose .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) .subscribe(pagingData -> pagingAdapter.submitData(lifecycle, pagingData));
Java
ExampleViewModel viewModel = new ViewModelProvider(this) .get(ExampleViewModel.class); UserAdapter pagingAdapter = new UserAdapter(new UserComparator()); RecyclerView recyclerView = findViewById<RecyclerView>( R.id.recycler_view); recyclerView.adapter = pagingAdapter // Activities can use getLifecycle() directly, but Fragments should instead use // getViewLifecycleOwner().getLifecycle(). viewModel.liveData.observe(this, pagingData -> pagingAdapter.submitData(getLifecycle(), pagingData));
RecyclerView
のリストに、データソースのページング データが表示され、必要に応じて別のページが自動的に読み込まれるようになりました。
読み込み状態を表示する
ページング ライブラリでは UI に読み込み状態が表示されます。これは LoadState
オブジェクトを介して行われます。LoadState
は、現在の読み込み状態に応じて、次の 3 つの形式のいずれかになります。
- アクティブな読み込みオペレーションがなく、エラーもない場合、
LoadState
はLoadState.NotLoading
オブジェクトになります。 - アクティブな読み込みオペレーションがある場合、
LoadState
はLoadState.Loading
オブジェクトになります。 - エラーがある場合、
LoadState
はLoadState.Error
オブジェクトになります。
UI で LoadState
を使用する方法は 2 つあります。リスナーを使用する方法と、特別なリスト アダプターを使用して読み込み状態を RecyclerView
リストに直接表示する方法です。
リスナーを使用して読み込み状態を取得する
UI で一般的に使用する読み込み状態を取得するために、PagingDataAdapter
には addLoadStateListener()
メソッドが含まれています。
Kotlin
// Activities can use lifecycleScope directly, but Fragments should instead use // viewLifecycleOwner.lifecycleScope. lifecycleScope.launch { pagingAdapter.loadStateFlow.collectLatest { loadStates -> progressBar.isVisible = loadStates.refresh is LoadState.Loading retry.isVisible = loadState.refresh !is LoadState.Loading errorMsg.isVisible = loadState.refresh is LoadState.Error } }
Java
pagingAdapter.addLoadStateListener(loadStates -> { progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.VISIBLE : View.GONE); retry.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.GONE : View.VISIBLE); errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error ? View.VISIBLE : View.GONE); });
Java
pagingAdapter.addLoadStateListener(loadStates -> { progressBar.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.VISIBLE : View.GONE); retry.setVisibility(loadStates.refresh instanceof LoadState.Loading ? View.GONE : View.VISIBLE); errorMsg.setVisibility(loadStates.refresh instanceof LoadState.Error ? View.VISIBLE : View.GONE); });
アダプターを使用して読み込み状態を表示する
ページング ライブラリには、LoadStateAdapter
という名前の別のリスト アダプターがあります。これは、表示されるページング データのリストで読み込み状態を直接表示するために使用します。
まず、LoadStateAdapter
を実装するクラスを作成し、onCreateViewHolder()
メソッドと onBindViewHolder()
メソッドを定義します。
Kotlin
class LoadStateViewHolder( parent: ViewGroup, retry: () -> Unit ) : RecyclerView.ViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.load_state_item, parent, false) ) { private val binding = LoadStateItemBinding.bind(itemView) private val progressBar: ProgressBar = binding.progressBar private val errorMsg: TextView = binding.errorMsg private val retry: Button = binding.retryButton .also { it.setOnClickListener { retry() } } fun bind(loadState: LoadState) { if (loadState is LoadState.Error) { errorMsg.text = loadState.error.localizedMessage } progressBar.isVisible = loadState is LoadState.Loading retry.isVisible = loadState is LoadState.Error errorMsg.isVisible = loadState is LoadState.Error } } // Adapter that displays a loading spinner when // state = LoadState.Loading, and an error message and retry // button when state is LoadState.Error. class ExampleLoadStateAdapter( private val retry: () -> Unit ) : LoadStateAdapter<LoadStateViewHolder()> { override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState ) = LoadStateViewHolder(parent, retry) override fun onBindViewHolder( holder: LoadStateViewHolder, loadState: LoadState ) = holder.bind(loadState) }
Java
class LoadStateViewHolder extends RecyclerView.ViewHolder { private ProgressBar mProgressBar; private TextView mErrorMsg; private Button mRetry; LoadStateViewHolder( @NonNull ViewGroup parent, @NonNull View.OnClickListener retryCallback) { super(LayoutInflater.from(parent.getContext()) .inflate(R.layout.load_state_item, parent, false)); LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView); mProgressBar = binding.progressBar; mErrorMsg = binding.errorMsg; mRetry = binding.retryButton; } public void bind(LoadState loadState) { if (loadState instanceof LoadState.Error) { LoadState.Error loadStateError = (LoadState.Error) loadState; mErrorMsg.setText(loadStateError.getError().getLocalizedMessage()); } mProgressBar.setVisibility(loadState instanceof LoadState.Loading ? View.VISIBLE : View.GONE); mRetry.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); mErrorMsg.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); } } // Adapter that displays a loading spinner when // state instanceOf LoadState.Loading, and an error message and // retry button when state instanceof LoadState.Error. class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> { private View.OnClickListener mRetryCallback; ExampleLoadStateAdapter(View.OnClickListener retryCallback) { mRetryCallback = retryCallback; } @NotNull @Override public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent, @NotNull LoadState loadState) { return new LoadStateViewHolder(parent, mRetryCallback); } @Override public void onBindViewHolder(@NotNull LoadStateViewHolder holder, @NotNull LoadState loadState) { holder.bind(loadState); } }
Java
class LoadStateViewHolder extends RecyclerView.ViewHolder { private ProgressBar mProgressBar; private TextView mErrorMsg; private Button mRetry; LoadStateViewHolder( @NonNull ViewGroup parent, @NonNull View.OnClickListener retryCallback) { super(LayoutInflater.from(parent.getContext()) .inflate(R.layout.load_state_item, parent, false)); LoadStateItemBinding binding = LoadStateItemBinding.bind(itemView); mProgressBar = binding.progressBar; mErrorMsg = binding.errorMsg; mRetry = binding.retryButton; } public void bind(LoadState loadState) { if (loadState instanceof LoadState.Error) { LoadState.Error loadStateError = (LoadState.Error) loadState; mErrorMsg.setText(loadStateError.getError().getLocalizedMessage()); } mProgressBar.setVisibility(loadState instanceof LoadState.Loading ? View.VISIBLE : View.GONE); mRetry.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); mErrorMsg.setVisibility(loadState instanceof LoadState.Error ? View.VISIBLE : View.GONE); } } // Adapter that displays a loading spinner when // state instanceOf LoadState.Loading, and an error message and // retry button when state instanceof LoadState.Error. class ExampleLoadStateAdapter extends LoadStateAdapter<LoadStateViewHolder> { private View.OnClickListener mRetryCallback; ExampleLoadStateAdapter(View.OnClickListener retryCallback) { mRetryCallback = retryCallback; } @NotNull @Override public LoadStateViewHolder onCreateViewHolder(@NotNull ViewGroup parent, @NotNull LoadState loadState) { return new LoadStateViewHolder(parent, mRetryCallback); } @Override public void onBindViewHolder(@NotNull LoadStateViewHolder holder, @NotNull LoadState loadState) { holder.bind(loadState); } }
次に、PagingDataAdapter
オブジェクトから withLoadStateHeaderAndFooter()
メソッドを呼び出します。
Kotlin
pagingAdapter .withLoadStateHeaderAndFooter( header = ExampleLoadStateAdapter(adapter::retry), footer = ExampleLoadStateAdapter(adapter::retry) )
Java
pagingAdapter .withLoadStateHeaderAndFooter( new ExampleLoadStateAdapter(pagingAdapter::retry), new ExampleLoadStateAdapter(pagingAdapter::retry));
Java
pagingAdapter .withLoadStateHeaderAndFooter( new ExampleLoadStateAdapter(pagingAdapter::retry), new ExampleLoadStateAdapter(pagingAdapter::retry));
RecyclerView
にヘッダーやフッターの読み込み状態のみを表示する場合は、withLoadStateHeader()
または withLoadStateFooter()
を呼び出します。
参考情報
ページング ライブラリについて詳しくは、以下の参考情報をご覧ください。