SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

一歩進んだAndroidアプリ開発ができる「Android Jetpack」入門

大量データを効率よく表示できる「ページングライブラリ」を解説

一歩進んだAndroidアプリ開発ができる「Android Jetpack」入門 第12回

  • X ポスト
  • このエントリーをはてなブックマークに追加

ページングライブラリの登場オブジェクトのコーディングパターン

 ページングライブラリを概観し、その利用の準備ができたところで、実際のコーディング方法を紹介していくことにしましょう。

 本連載では、これまで、まずはJavaコードを紹介し、その後Kotlinコードを紹介するという流れでした。一方、ページングライブラリは、図2および表1にFlowオブジェクトが登場することからもわかるように、Kotlinでのコーディングが基本となっています。そのため、これまでとは逆に、まずKotlinコードを紹介し、その後Javaコードを紹介することにします。

RoomによるPagingSourceの生成

 まずはPagingSourceオブジェクトです。このオブジェクトはリポジトリで用意することになっています。実際の生成は、Roomを利用する場合、非常に簡単です。リスト2のように、DAOインターフェースのリストデータを取得するメソッドの戻り値の型として、PagingSourceとするだけです。

[リスト2]ページングライブラリを利用するPhoneDAOインターフェース(Kotlin版)
@Dao
interface PhoneDAO {
  @Query("SELECT * FROM phones ORDER BY id")
  fun findAll(): PagingSource<Int, Phone>
}

 その際、ジェネリクスとして2個のデータ型を指定します。ひとつめは、各ページを区別するためのデータのデータ型を指定します。例えば、1ページに50件分のデータを取得する場合、つまり、スクロールしていくとある段階で次の50件を取得するといった場合、どのようなデータをもとに次の50件とするかを判定するためのデータのデータ型です。ここで題材としている電話番号リストの場合は、そのidの値、すなわち、整数値を利用すれば次の50件を取得できます。そこで、リスト2ではIntとしています。

 ジェネリクスのふたつめは、リストデータの各アイテムを表すクラスです。Roomを利用する場合は、エンティティクラスそのものですので、リスト2で指定しているPhoneエンティティは、リスト3のコードとなります。

[リスト3]Phoneエンティティクラス(Kotlin版)
@Entity(tableName = "phones")
data class Phone(
  @PrimaryKey(autoGenerate = true)
  val id: Long,
  val phoneNo: String
)

 このようにして用意したPhoneDAOのfindAll()を、リポジトリでは、リスト4のように実行して、その戻り値をリターンすることで、PagingSourceオブジェクトの用意ができたことになります。

[リスト4]リポジトリクラス(Kotlin版)
class PhoneRepository(application: Application) {
  :
  fun getAllPhoneListPagingSource(): PagingSource<Int, Phone> {
    val phoneDAO = _db.createPhoneDAO()
    return phoneDAO.findAll()
  }
}

ViewModelでのFlowの抽出

 次に、図2の通り、ViewModelにおいて、リポジトリからPagingSourceオブジェクトを取得した上で、Flowオブジェクトを抽出します。これはリスト5のコードとなります。

[リスト5]ViewModelクラス(Kotlin版)
private const val ITEMS_PER_PAGE = 50  // (1)
class MainViewModel(application: Application) : AndroidViewModel(application) {
  val phoneListFlow: Flow<PagingData<Phone>>  // (2)
  private val _phoneRepository: PhoneRepository
  init {
    _phoneRepository  = PhoneRepository(application)
    val phoneListPageSource = _phoneRepository.getAllPhoneListPagingSource()  // (3)
    val pagingConfig = PagingConfig(ITEMS_PER_PAGE)  // (4)
    val phoneListPager = Pager(pagingConfig) {phoneListPageSource}  // (5)
    phoneListFlow = phoneListPager.flow.cachedIn(viewModelScope)  // (6)
  }
}

 大まかな流れとしては、図2の通り、リポジトリから取得したPagingSourceオブジェクトとPagingConfigオブジェクトをもとに、Pagerオブジェクトを生成し、そのPagerオブジェクトからFlowオブジェクトを抽出する処理となります。

 まず、リスト5の(3)がリポジトリからPagingSourceオブジェクトを取得している処理で、phoneListPageSourceとしています。そして、Pagerオブジェクトを生成する前に、PagingConfigオブジェクトを生成しています。それが(4)のコードです。これは表1の通り、ページ分割に関する設定情報を保持するオブジェクトであり、少なくともインスタンス生成時に、すなわち、コンストラクタの引数として1ページあたりの取得件数を指定する必要があります。リスト5では(1)の定数を指定し、50件としています。

 これでPagerオブジェクトを生成する準備ができたので、インスタンスを生成します。その際、第1引数としてPagingConfigオブジェクトを、第2引数としてPagingSourceオブジェクトをリターンするラムダ式を記述します。それが(5)です。Pager(pagingConfig)の()内が第1引数であるPagingConfigオブジェクトのpagingConfigであり、{phoneListPageSource}がPagingSourceオブジェクトであるphoneListPageSourceをリターンするラムダ式です。

 このようにして生成したPagerオブジェクトからFlowを抽出します。それが(6)のコードです。Pagerオブジェクトは、そのプロパティであるflowがそのままFlowオブジェクトを表すので、それを取得します。ただし、そのFlowオブジェクトをそのままViewModelのプロパティとしてアクティビティなどで利用するのではなく、cachedIn()メソッド(より正確には拡張関数)を実行した戻り値を利用します。このcachedIn()メソッドは、そのメソッド名の通り、PagingSourceによって生成される各ページデータをキャッシュしながら、アクティビティなどがやり取りできる形で用意してくれる働きがあります。その際、コルーチンスコープを引数として渡す必要があり、そのスコープに連動してページデータのロードが行われるようになります。

 cachedIn()メソッドの戻り値もまたFlowオブジェクトなので、(6)ではそれを(2)のプロパティとしています。その(2)のプロパティを見ると、Flowの構成要素がPagingDataとなっています。このPagingDataはまさにページデータを表すオブジェクトであり、ページデータの元となるリストのアイテム、つまりエンティティのデータ型をジェネリクスとして指定する必要があります。(2)ではPhoneとしています。

アダプタとアイテム比較オブジェクトの用意

 リスト5のコードで、ページデータのFlowオブジェクトが、ViewModelのプロパティとして用意され、アクティビティやフラグメントからはいつでも利用できるようになりました。そのアクティビティなどでは、FlowデータとRecyclerViewとを連携させる必要があります。その際、本来ならば第5回で紹介した方法で、オブザーバやコレクタから変更処理を通知できるアダプタクラスを作成する必要があります。しかし、ページングライブラリではFlow<PagingData>と連携するための専用のページング用アダプタクラスが用意されており、それを利用することになります。それが、表1のPagingDataAdapterであり、リスト6のコードとなります。

[リスト6]PagingDataAdapterクラス(Kotlin版)
private inner class PhoneListAdapter(diffCallback: DiffUtil.ItemCallback<Phone>) : PagingDataAdapter<Phone, PhoneViewHolder>(diffCallback) {  // (1)
  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PhoneViewHolder {  // (2)
    :
    return PhoneViewHolder(row)
  }
  override fun onBindViewHolder(holder: PhoneViewHolder, position: Int) {  // (3)
    val item: Phone? = getItem(position)  // (4)
    holder.bind(item)
  }
}

 ページング用アダプタクラスは、リスト6の(1)のように、まさにPagingDataAdapterクラスを継承して作ります。その際、ジェネリクスのひとつめにエンティティを、ふたつめにビューホルダクラスを指定します。そして(2)のようにonCreateViewHolder()メソッドを、(3)のようにonBindViewHolder()メソッドを実装します。これらのメソッドの働きは、通常のアダプタクラスと同様です。

 なお、PagingDataAdapterクラスには、ポジションから各アイテムデータを取り出すメソッドとしてgetItem()があるので、(4)のようにonBindViewHolder()ではこのメソッドによって取り出したアイテムデータ、つまりエンティティを利用します。

 ただし、PagingDataAdapterクラスは、コンストラクタの引数としてDiffUtil.ItemCallbackオブジェクトを必要とします。そのため、継承した子クラスであるPhoneListAdapterでもコンストラクタの引数として定義しておき、それをそのまま親クラスに渡すコードとします。

 そして、アクティビティなどでPhoneListAdapterのインスタンス生成時に、DiffUtil.ItemCallbackインスタンスを渡すようにします。DiffUtil.ItemCallbackクラスは、リスト7のような内容となります。

[リスト7]DiffUtil.ItemCallbackクラス(Kotlin版)
private class PhoneComparator : DiffUtil.ItemCallback<Phone>() {  // (1)
  override fun areItemsTheSame(oldItem: Phone, newItem: Phone): Boolean {  // (2)
    return oldItem.id == newItem.id
  }

  override fun areContentsTheSame(oldItem: Phone, newItem: Phone): Boolean {  // (3)
    return oldItem == newItem
  }
}

 DiffUtil.ItemCallbackクラスそのものは抽象クラスなので、子クラスを作成します。リスト7では、(1)のようにPhoneComparatorとしています。その際、ジェネリクスとしてエンティティを指定します。そして、子クラスではareItemsTheSame()メソッドとareContentsTheSame()メソッドを実装する必要があります。

 このクラスは表1の通り、2種のリスト内の各アイテムが同じものかどうかを判定するためのオブジェクト、いわばアイテム比較オブジェクトとなっています。アダプタはこのオブジェクトのareItemsTheSame()メソッドとareContentsTheSame()メソッドを利用して、各アイテムが同じかどうかを判定し、リストの再利用を行うかどうかを判断しています。そのため、(2)のareItemsTheSame()メソッドでは、2種のアイテムの対象が同じかどうかを判定します。これは、通常主キーの値が同じかどうかで判定します。一方、(3)のareContentsTheSame()メソッドは、アイテムに含まれているデータそのものが全て同じかどうかを判定します。

アクティビティなどでのアダプタとFlowの連携

 さあ、最後の仕上げです。アダプタオブジェクトをRecyclerViewにセットし、Flowとアダプタを連携させましょう。これは、リスト8のコードとなります。

[リスト8]MainActivityでの連携コード(Kotlin版)
override fun onCreate(savedInstanceState: Bundle?) {
  :
  val adapter = PhoneListAdapter(PhoneComparator())  // (1)
  _activityMainBinding.rvPhoneList.adapter = adapter  // (2)

  lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
      _mainViewModel.phoneListFlow.collectLatest {  // (3)
        adapter.submitData(it)  // (4)
      }
    }
  }
}

 リスト8の(1)は、リスト6のPhoneListAdapterインスタンスを生成しているコードであり、その際、リスト7のPhoneComparatorインスタンスを渡しています。これは、前項の解説のとおりです。このようにして生成したアダプタをRecyclerViewにセットしているのが、(2)のコードです。

 このアダプタオブジェクト、すなわちPagingDataAdapterオブジェクトには、Flow中のデータ変更に合わせてアダプタ内のデータを自動的に変更するメソッドがあらかじめ備わっています。それが(4)のsubmitData()メソッドです。本来ならば、第5回で紹介したように、このようなメソッドはアダプタクラスに自作する必要がありますが、PagingDataAdapterでは不要です。

 あとは、Flowオブジェクトのコレクタ内でこのsubmitData()を実行し、引数として新しいデータを渡すだけです。ただし、ページングライブラリでは、(3)のように、Flowのcollect()メソッドの代わりに、collectLatest()メソッドを実行することが推奨されています。collect()とcollectLatest()との違いは、そのメソッド名の通り、Flow中のデータ更新が行われた場合、もしコレクタ内の処理が途中の場合は、それを破棄して、最新のもののみを採用することになっている点です。そこで、このcollectLatest()を実行し、ラムダ式内でsubmitData()を実行しています。その際、引数として渡しているitがcollectLatest()のラムダ式の引数、つまり、新しいデータを表します。

次のページ
Javaでのページングライブラリの利用

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
一歩進んだAndroidアプリ開発ができる「Android Jetpack」入門連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 齊藤 新三(サイトウ シンゾウ)

WINGSプロジェクトについて>有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティ(代表 山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手がける。2018年11月時点での登録メンバは55名で、現在も執筆メンバを募集中。興味のある方は、どしどし応募頂きたい。著書記事多数。 RSS X: @WingsPro_info(公式)、@WingsPro_info/wings(メンバーリスト) Facebook <個人紹介>WINGSプロジェクト所属のテクニカルライター。Web系製作会社のシステム部門、SI会社を経てフリーランスとして独立。屋号はSarva(サルヴァ)。HAL大阪の非常勤講師を兼務。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるReact実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/20372 2024/10/28 11:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング