APIから取得したデータをDBに保存してキャッシュをする場合
APIから取得したデータをDBに保存してキャッシュをする場合の実装は「APIから取得したデータを表示するだけの場合」よりも難しくなります。実装する順番としては以下になります。
- DBに保存するKeyとAnimeのエンティティを定義する
- DBからデータを取り出し、PagingSourceを返却するメソッドをもつDaoを実装する
- APIから取得したデータをキャッシュするDBを実装する
- APIからデータを取得し、DBに保存するRemoteMediatorの実装
- PagerのpagingSourceFactoryにDaoで作成したメソッドを指定する
順を追って説明していきます。完成系はサンプルコードのfeature/remote_mediatorをご覧ください。
APIから取得したデータをキャッシュするDBを実装する
APIから取得したデータをキャッシュするDBを作る際に、保存しておくデータは2つあります。APIから取得したデータ(今回はAnimeクラス)とデータ取得に使用するpageの保存です。
pageは保存しなくとも、「APIから取得したデータを表示する」の際に実装したようにstateからpageを取得することができますが、関心の分離の観点から公式ではDBなどに保存することが推奨されています。
今回はsessionIdなどを用いて、業務で使用するような実践的な実装を紹介するため、DBにpageを保存する手法を用いますがpageを保存せずに、stateからpageを取得することで実装は可能です。
Animeエンティティの実装
まず初めに、DBに保存するEntityを作っていきます。今回、APIから返却されたAnimeクラスを保存するうえ、実装するクラスを減らすため、AnimeクラスをそのままDBのエンティティとして扱います。
Animeには一意のidがidフィールドに入っているためそれをPrimaryKeyとして指定します。
また、別の読み込みのアニメが表示されないようにするため、sessionIdをEntityに追加します。sessionIdはAPIから返却されないため、初期値を設定する必要があり、sessionIdはStringなので、空文字列を指定します。
その際の注意点として、データクラスなどをDBに保存する際は、データベースに保存できるようにするため、Serializableを実装する必要があります。
すべてを記述すると長くなるため、ポイントのみ抜粋してAnimeクラスを記述すると以下のようになります。
// テーブル名を"anime"としてEntityに指定する @Entity(tableName = "anime") data class Anime( @PrimaryKey val id: Int, val sessionId: String = "", ... // Gson等によるコンバートができるようにSerializableを実装させます ): Serializable { .... }
Keyエンティティの実装
Animeエンティティを作成したところで、keyを保存するためのエンティティとしてPageKeyを作成します。
PageKeyに保存するデータは、以下の3つです。
- id:PrimaryKeyとして指定するためのカラムとしてautoGenerateをtrueに指定して用意します。
- sessionId:読み込みごとにセッションIDを生成しないと、どのタイミングの読み込みかわからないため識別子として用意します。Pagingの実装上は必須ではないですが、あると正しくデータを読み込めるため設定することをおすすめします。
- nextKey:次の読み込みの際に使用するkeyを保存するため用意します。
実装例としては以下のようになります。
@Entity(tableName = "page_key") data class PageKey( @PrimaryKey(autoGenerate = true) val id: Int = 0, val sessionId: String, val nextKey: Int?, )
TypeConverterの実装
DBに保存するエンティティの中に、DBにそのまま保存することができない型がある場合は適宜TypeConverterを実装し、DataBaseに指定する必要があります。次はその実装を行います。
必要になる場合のサンプルケースとしては以下になります。
@Entity(tableName = "anime") data class Anime ( @PrimaryKey val id : String, // <Converter必要なし val image: Images, // <Converter必要あり ) { data class Images ( imageUrl: List<String>, ) }
TypeConverterは、Convert対象のクラスからStringへの変換とStringからそのクラスへの変換の2パターン必要になります。そのままDBに保存できない型の分だけTypeConverterを実装してください。似たような実装になりますので、今回は1つのみサンプルコードを記載します。
Gsonを使う場合のTypeConverterは以下のようになります。
@TypeConverter @JvmStatic fun imagesToString(data: Anime.Images?): String? { return data?.let{ Gson().toJson(it) } } @TypeConverter @JvmStatic fun stringToImages(data: String?): Anime.Images? { if (data == null) return null val dataType = object : TypeToken<Anime.Images>() {}.type return Gson().fromJson(data, dataType) }
余談ではありますが、業務で使用する場合単体テストを記述すると思います。その場合withContextのコンテキストにDispatchers.IOを指定するよりも、CoroutineDispatcherをRemoteMediatorの引数に追加し、それでlaunchするとよりテストが記述しやすくなります。