SHOEISHA iD

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

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

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

「ページングライブラリ」でデータベースにキャッシュしながらページングする方法

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


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

Kotlin版RemoteMediatorのコーディングとその利用方法

 準備ができたところで、早速RemoteMediatorを作成していきましょう。

RemoteMediatorのコードパターン

 RemoteMediatorのKotlin版のコード例は、リスト2のようになります。

[リスト2]data/remote/PhoneRemoteMediator.kt
@OptIn(ExperimentalPagingApi::class)  // (1)
class PhoneRemoteMediator(
  private val _db: AppDatabase  // (2)
) : RemoteMediator<Int, Phone>() {  // (3)
  override suspend fun load(loadType: LoadType, state: PagingState<Int, Phone>): MediatorResult {  // (4)
    //戻り値変数を用意。
    var returnVal: MediatorResult  // (5)
    val phoneDAO = _db.createPhoneDAO()
    try {
      //loadTypeで処理を分岐。
      when(loadType) {  // (6)
        //データクリアの場合。
        LoadType.REFRESH -> {  // (7)
          phoneDAO.deleteAll()
          returnVal = MediatorResult.Success(false)
        }
        //データの末尾を検知した場合。
        LoadType.PREPEND -> {  // (8)
          returnVal = MediatorResult.Success(true)
        }
        //データの先頭を検知した場合。
        LoadType.APPEND -> {  // (9)
          //データベース内のidの最大ちからstartKeyとendKeyを決定。
          val lastId = phoneDAO.findLastId()  // (10)
          val startKey = lastId + 1
          val endKey = startKey + state.config.pageSize - 1  // (11)
          //ネット上から追加のリストを取得しデータベースに格納。
          val fetchedPhoneList = fetchPhoneList(startKey, endKey)  // (12)
          phoneDAO.insertPhoneList(fetchedPhoneList)  // (13)
          //ネット上からリストサイズを取得してリスに続きがあるかどうかを判定。
          val listSize = fetchPhoneListSize()  // (14)
          if(endKey < listSize) {
            returnVal = MediatorResult.Success(false)  // (15)
          }
          else {
            returnVal = MediatorResult.Success(true)  // (16)
          }
        }
      }
    }
    catch(ex: Exception) {
      returnVal = MediatorResult.Error(ex)  // (17)
    }
    return returnVal  // (18)
  }
}

 RemoteMediatorを作成する場合の大きなルールをまとめると、次のようになります。

RemoteMediatorクラスを継承したクラスとする

 リスト2の(3)が該当します。その際、PagingSourceのジェネリクスとして記述していたもの、すなわち、ジェネリクスのひとつめは、各ページを区別するためのデータのデータ型を、ふたつめはリストデータの各アイテムを表すクラスを指定します。

クラスに@OptInアノテーションを付与する

 リスト2の(1)が該当します。この記述がないとコンパイルエラーとなります。これは、実験的なAPIには付与しなければならないアノテーションです。この例であれば、Pagingライブラリで導入されている新しい機能を試験的に利用できるようになります。

AppDatabaseをコンストラクタの引数として定義する

 リスト2の(2)が該当します。先述のとおり、RemoteMediatorではRoomのDAOクラスを利用してデータベースへのアクセスが必要です。そのためにはAppDatabaseをあらかじめ取得しておく必要があります。

load()メソッドをオーバーライドする

 リスト2の(4)が該当します。RemoteMediatorクラスは抽象クラスであり、抽象メソッドであるload()をオーバーライドする必要があります。そして、このメソッド内にネットからデータを取得し、データベースにキャッシュするコードを記述します。

load()メソッドの構文

 そのload()メソッドの構文を分解すると、表1のようになります。

表1:load()メソッドの構文
第1引数 loadType: LoadType データベース内のデータに対してどのような処理を行えばよいのかを表す値
第2引数 state: PagingState ページングの現在の状態に関するデータを格納したオブジェクト
戻り値 MediatorResult リストデータの取得結果を表すオブジェクト

load()メソッドの戻り値

 表1の内容のうち、まず、戻り値から説明することにします。これは表1およびリスト2の(4)の通り、MediatorResultです。

 これは、表2の3種のインスタンスを生成してリターンします。

表2:load()メソッドの戻り値
インスタンス生成コード インスタンスを生成するパターン
MediatorResult.Success(false) データ取得に成功し、次ページデータが存在する場合
MediatorResult.Success(true) データ取得に成功し、次ページデータが存在しない、つまりリストデータの最後に到達した場合

MediatorResult.Error(例外インスタンス)

データ取得が失敗した場合

 Success()によるインスタンス生成時の引数のbool値に関しては、その引数名がendOfPaginationReachedと定義されていることから、リストデータの最後に到達しているかを表す引数です。そのことを踏まえると、trueなのかfalseなのかがわかりやすいと思います。

 リスト2では(5)で戻り値としてリターンするMediatorResult型の変数returnValを用意し、最終的に(18)でリターンするコードパターンとしています。その途中では、try-catch構文を利用し、tryブロック中にデータ取得、およびデータベースへの格納コードを記述し、その結果に応じて、Success(false)かSuccess(true)をreturnValに格納しています。

 一方、(17)のcatchブロック内でreturnValにError(ex)を格納しています。このコードパターンを採用したのは、前回同様、ネット上からデータを取得する処理では必須のコードパターンだからです。

データ処理コードは引数loadTypeで分岐

 では、tryブロックの中、すなわちデータ処理コードを見ていくことにしましょう。

 その際、load()メソッドの第1引数loadTypeの値で分岐を行います。このloadTypeはEnumのLoadTypeであり、その値は表3の3個のどれかです。

表3:loadTypeの値
内容
REFRESH PagingSourceが破棄されるなど、キャッシュをクリアしてデータを再取得する必要がある場合
APPEND データベース内のデータの末尾を検知し、その続きのデータを取得する必要がある場合
PREPEND データベース内のデータの先頭を検知し、その続きのデータを取得する必要がある場合

 値が3個なので、リスト2では(6)でloadTypeの値を元にしたwhen構文を利用して分岐処理を行っています。

 そのうち、(7)がREFRESHの場合です。この場合は、表3の通り、データのクリアが必要なので、リスト1でPhoneDAOに用意した全削除メソッドのdeleteAll()を実行し、データを削除します。その後、戻り値をSuccess(false)としています。falseとしているのは、データベース内のデータが空なので、最初からのデータが必要だからです。

 (8)は、PREPENDの場合です。これは表3の通り、データベース内のデータの先頭を検出した場合です。先頭よりさらにその前にデータを追加する必要がある場合に利用します。例えば、ネット上のリストデータが1000件あるとして、初期リスト画面が1から始まるのではなく、リストの末尾の980~1000件や、途中の500~520件を表示する場合、リストをさかのぼるスクロールが発生します。この場合は、PREPENDが渡されるので、それに合わせて980以前、あるいは500以前のデータを取得してデータベースに格納する処理を行います。ただし、今回はリストを最初から表示させる仕様のアプリなので、(8)ではSuccess(true)を戻り値とし、前のデータが存在しないとしています。

 そして(9)が、APPENDの場合であり、今回のメインの処理です。その処理内容は(10)で、リスト1でPhoneDAOに追加したfindLastId()メソッドを使って、データベース内のidの最大値を取得し、その値に1足して開始キーとしています。

 次に、終了キーがわかれば、ネットからリストデータを取得できます。その際、1ページ分のアイテム数データが必要で、その値はload()の第2引数PagingStateから取得できます。このオブジェクトのconfigプロパティは、PagingConfigオブジェクトです。そのpageSizeプロパティに1ページ分のアイテム数が格納されているので、(11)ではこれを基に取得するリストデータの終了キーを計算しています。

 これらの開始キーと終了キーを元に、ネットからリストデータを取得しているのが(12)です。このコードパターンは、前回のリスト2のPagingSourceクラスのload()メソッド内のコードパターンと同じです。

 異なるのは(13)でネットから取得したリストデータをデータベースに格納している点です。その際、リスト1でPhoneDAOに追記したinsertPhoneList()を利用します。

 最終的に、データの取得と格納処理が終了したら、戻り値を確定する必要があります。そこで(14)でネット上のリストのサイズを取得し、終了キーとそのリストサイズを比較し、リストサイズの方が大きい場合、続きのリストデータが存在するとして(15)のように戻り値Success(false)とします。

 逆に、リストサイズの方が小さい場合は続きのデータがないとして(16)のように戻り値をSuccess(true)とします。

リポジトリでのRemoteMediatorの利用

 これで、RemoteMediatorクラスが完成しました。今度は、リポジトリでのPagerの生成時に、RemoteMediatorを利用するコードパターンを紹介します。

 これは、リスト3のようになります。

[リスト3]data/repository/PhoneRepository.kt
@OptIn(ExperimentalPagingApi::class)  // (1)
fun getAllPhoneListPager(): Pager<Int, Phone> {
  val phoneDAO = _db.createPhoneDAO()
  val pagingConfig = PagingConfig(ITEMS_PER_PAGE)
  val phoneListPager = Pager(pagingConfig, null, PhoneRemoteMediator(_db)) { phoneDAO.findAll() }  // (2)
  return phoneListPager
}

 リスト3のポイントは、Pagerインスタンスを生成する(2)のコードです。

 Pagerインスタンスを生成する際の第1引数であるPagingConfigインスタンス、ラムダ式内でPagingSourceオブジェクトをリターンするコード記述は、これまで同様です。リスト3ではRoomを利用するために、ラムダ式内はPhoneDAOのfindAll()メソッドの実行コードを記述しています。

 変わるのは第2引数と第3引数です。その第3引数として、自作したRemoteMediatorのインスタンスを渡します。なお、第2引数は、initialKeyという引数であり、初期キー値を渡します。ただし、これは省略可能であり、その場合はリスト3の(2)のようにnullを渡します。

 さらに、リスト2の(1)でRemoteMediatorクラスに付記したアノテーションがリポジトリメソッドにも必要であり、それが(1)です。こちらも忘れるとコンパルエラーとなるので注意してください。

次のページ
Java版RemoteMediatorのコーディングとその利用方法

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

  • 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/21385 2025/04/25 17:53

おすすめ

アクセスランキング

アクセスランキング

イベント

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

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

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

メールバックナンバー

アクセスランキング

アクセスランキング