0
0

More than 1 year has passed since last update.

vol.1/2 APIのGET結果をキャッシュする (Room+Hilt導入編)

Last updated at Posted at 2023-07-19

APIのGET結果をキャッシュしたい!

天気予報アプリも順調に育ってきました。
「画面を表示」 -> 「APIコール」 -> 「表示」はできましたので、APIコールの回数を減らすべく、10分以内のリクエストならDBを参照する様にしたいと思います。

ということで、キャッシュ先をRoomにすべく、導入していきます。

Room

1. gradleの定義

まずは、Roomの依存定義を行います。

app/build.gradle
plugins {
    id 'org.jetbrains.kotlin.android'
+    id 'kotlin-kapt'
    id 'com.google.dagger.hilt.android'
}

android {
    defaultConfig {
+        kapt {
+            arguments {
+                // ① schema定義を出力しておくと、AutoMigrationできるようになるので
+                //    出力先を指定します
+                arg("room.schemaLocation", "$projectDir/schemas")
+            }
+        }
    }
}

dependencies {
+    // Room
+    def room_version = "2.5.2"
+    implementation "androidx.room:room-runtime:$room_version"
+    annotationProcessor "androidx.room:room-compiler:$room_version"
+    implementation "androidx.room:room-ktx:$room_version"
+    kapt "androidx.room:room-compiler:$room_version"
}

①「arg("room.schemaLocation", "$projectDir/schemas")」の部分は、後から作成するDatabaseクラスにしている「exportSchema=true」(意味:スキーマ定義をエクスポートするよ)の時に必要になる、スキーマ定義ファイルの出力先を指定しています。

2. データエンティティクラスの作成

地域情報(Areaドメイン)を保存するためのEntity定義をおこないます。
参考:Room エンティティを使用してデータを定義する by Google developers

Areaのソース
Area
data class Area(
    val centers: List<Center>,
)

data class Center(
    val code: String,
    val name: String,
    val offices: List<Office>,
)

data class Office(
    val code: String,
    val name: String,
)

center:office = 1:m という関係なので、それをRDB的に表現します。

CenterEntity.kt
@Entity(tableName = "center")
data class CenterEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "code") val code: String,
    @ColumnInfo(name = "name") val name: String,
)
OfficeEntity
@Entity(tableName = "office")
data class OfficeEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "code") val code: String,
    @ColumnInfo(name = "name") val name: String,
    @ColumnInfo(name = "center_code") val centerCode: String,
)

3. DAOの作成

Room DAO を使用してデータにアクセスする by Google develpers
CenterEntityとOfficeEntityにアクセスするための、AreaDaoクラスを作ります。
Entity:DAO=1:1となるように作るのもありだと思います。

地域情報はマスタデータで、全レコード削除 -> 全レコード挿入という操作を想定しています。

AreaDao
@Dao
interface AreaDao {
    @Query("select * from center " +
           "join office on center.code = office.center_code " +
           "order by center.code, office.code")
    suspend fun getAll(): Map<CenterEntity, List<OfficeEntity>>

    @Query("delete from center")
    suspend fun deleteAllCenter()

    @Query("delete from office")
    suspend fun deleteAllOffice()

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertCenter(centerEntity: CenterEntity)

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertAllOffice(officeEntityList: List<OfficeEntity>)
}

getAll()の部分は、マルチマップを使用しています。
複数のテーブル定義を格納するためのデータクラスを追加で準備しなくてもいいので、とても便利ですね。(Room 2.4以降です)

@Insert(onConflict = OnConflictStrategy.REPLACE)のonConflictは不要ですが、クラッシュしにくくするために、念のため書いていてます。

4. Databaseクラス

Room関連最後のクラス、Databaseクラスを作成します。

AppDatabase.kt
@Database(
    entities = [CenterEntity::class, OfficeEntity::class],
    version = 1,
    exportSchema = true,
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun areaDao(): AreaDao
}

versionは、DBのマイグレーション判定に使用するデータベースのバージョンです。
テーブルの追加や削除、カラムの追加、変更、削除など、データベースに関する変更があった場合インクリメントしていきます。

exportSchemaは、マイグレーションが必要な場合に、手動でマイグレーションを記述しないで済むAutoMigrationを使用する際には、必ずtrueにします。この定義と併せて、build.gradleにSchema定義のexport先ディレクトリを指定する記述が必要です。

5. HiltのModuleにProviderを追加

HiltのModuleに、DatabaseクラスとDaoクラスのprovider定義を追加します。

InfrastructureModule.kt
    @Singleton
    @Provides
    fun provideAppDatabase(
        @ApplicationContext context: Context,
    ): AppDatabase {
        return Room.databaseBuilder(
            context = context,
            klass = AppDatabase::class.java,
            name = "forecast_db",
        ).build()
    }

    @Singleton
    @Provides
    fun provideAreaDao(
        db: AppDatabase,
    ): AreaDao {
        return db.areaDao()
    }

DAO経由でデータアクセスする

データ取得

まずは、AreaDao#getAll()を読んでみます。

AreaRepositoryImpl.kt
class AreaRepositoryImpl @Inject constructor(
    private val forecastApi: ForecastApi,
+    private val areaDao: AreaDao,
    private val dispatchers: CoroutineDispatcher,
) : AreaRepository {

+    private fun getAreaFromLocal(): Flow<Future<Area>> {
+        return flow<Future<Area>> {
+            val areaMap = areaDao.getAll()
+            val area = AreaAdapter.adaptFromDb(areaMap)
+            emit(Future.Success(area))
+        }.catch { cause ->
+            emit(Future.Error(cause))
+        }.flowOn(dispatchers)
+    }

}

flowの中で、データベースから地域情報を取得し、Adapterを経由して、Areaドメインに変換しています。

AreaAdapterとFutureのソース(興味があれば展開してください)
AreaAdapter.kt
object AreaAdapter {
    fun adaptFromDb(areaEntity: Map<CenterEntity, List<OfficeEntity>>): Area {
        return Area(
            centers = areaEntity.entries.map { (centerEntity, officeEntityList) ->
                Center(
                    code = centerEntity.code,
                    name = centerEntity.name,
                    offices = officeEntityList.map { officeEntity ->
                        Office(
                            code = officeEntity.code,
                            name = officeEntity.name,
                        )
                    }
                )
            },
        )
    }
}
Future.kt
sealed class Future<out T> {
    object Idle : Future<Nothing>()
    object Proceeding : Future<Nothing>()
    data class Success<out T>(val value: T) : Future<T>()
    data class Error(val error: Throwable) : Future<Nothing>()
}

データ削除と挿入(トランザクションも使う)

次にデータの削除と挿入をやってみます。

AreaRepositoryImpl.kt
class AreaRepositoryImpl @Inject constructor(
    private val forecastApi: ForecastApi,
+    private val db: AppDatabase,
    private val areaDao: AreaDao,
    private val sharedPref: SharedPref,
    private val dispatchers: CoroutineDispatcher,
) : AreaRepository {
+    private fun saveArea(area: Area): Flow<Future<Area>> {
+        return flow<Future<Area>> {
+            db.withTransaction {
+                areaDao.deleteAllCenter()
+                areaDao.deleteAllOffice()
+
+                area.centers.forEach { center ->
+                    val centerEntity = CenterEntity(code = center.code, name = center.name)
+                    val officeEntityList = center.offices.map { office ->
+                        OfficeEntity(code = office.code, name = office.name, centerCode = center.code)
+                    }
+
+                    areaDao.insertCenter(centerEntity)
+                    areaDao.insertAllOffice(officeEntityList)
+                }
+            }
+            emit(Future.Success(area))
+        }.catch { cause ->
+            emit(Future.Error(cause))
+        }.flowOn(dispatchers)
+    }
}

同一トランザクジョンで、データの削除と挿入を行う場合は、「withTransaction」を使用します。
withTransactionはsuspend関数です。
not suspendの時は、「runInTransaction」を使用します。
(ちなみに、withTransactionは、「androidx.room:room-ktx」への依存関係が必要になります。)

Insertの際にエンティティのインスタンスを作成しますが、今回、エンティティに定義してるidはAutoGeneratedです。
その場合、idには0を指定するのがお約束です。
リファレンス by Google developers

CenterEntity.kt
data class CenterEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
}

完成

完成コードはこちらです。

vol.2/2 APIのGET結果をキャッシュする (FlowChain,例外処理編)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0