Roomを用いたAndroidのデータ永続化

作成: 2020年09月22日

更新: 2021年02月01日

概要

Androidアプリをタスクキルすると基本的にはデータはすべて消え初期状態になる.そうなっては困るデータは何らかの方法でファイルに保存する必要がある.Androidではそういった用途のためにSharedPreferencesRoomがある.SharedPreferencesはxmlファイルに以下のように連想配列のような形式で書き込むことでデータを保持する.

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <boolean name="boolean" value="true" />
    <string name="string">hoge</string>
    <set name="string-set">
        <string>fuga</string>
        <string>piyo</string>
    </set>
    <int name="int" value="0" />
</map>

比較的データの書き込み,読み込みが簡単でアプリの設定の値などを保存するには優れているが可変長配列のような複雑な形式には向かない.複雑な形式のデータを保存するにはSQLiteに保存するRoomを使う必要があるがデータの書き込み,読み込みになかなか癖があるので備忘録として残す.

Roomの使用にはアプリのbuild.gradleに以下の依存関係が必要

build.gradledependencies {
  def room_version = "2.2.5"

  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"

  // optional - Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:$room_version"

  // optional - Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
}

Roomに必要なクラス

room_architecture.png

Roomでは上記のように実際にデータベースとやり取りするDatabaseクラスとデータベースアクセスに使用するメソッドを格納するDAO(Data Access Objects)とデータの形式を記述したEntitiesの3つで構成される.

今回はアラームアプリで用いたローカル音楽ファイルのデータ保存のためのクラスを例にとる.

データベース

SoundDatabase.kt@Database(entities = [Sound::class], version = 1)
abstract class SoundDatabase : RoomDatabase() {
    abstract fun soundDao(): SoundDao

    companion object {
        private const val dbName = "sabi_alarm_sounds.db"
        private var instance: SoundDatabase? = null

        fun getInstance(context: Context): SoundDatabase {
            if (instance == null) {
                instance = Room.databaseBuilder(context, SoundDatabase::class.java, dbName)
                    .fallbackToDestructiveMigration()
                    .build()
            }
            return requireNotNull(instance)
        }
    }
}

データベースクラスではデータベースの名前(dbName)を設定してデータベースにアクセスするインスタンスを作成する.companion objectにすることでどこから呼び出しても同じインスタンスを返すようにしている.また初回だけ実際にインスタンスを生成し2回目以降は同じインスタンスを返すようにしている.データベースの構造を変更した際はversion = 1の部分を増加させる必要がある.

DAO

SoundDao.kt@Dao
interface SoundDao {
    @Query("SELECT * FROM sounds")
    suspend fun getAll(): List<Sound>

    @Query("SELECT * FROM sounds WHERE id IN (:soundIds)")
    suspend fun loadAllByIds(soundIds: IntArray): List<Sound>

    @Insert
    suspend fun insertAll(vararg sounds: Sound)

    @Update
    suspend fun update(vararg sounds: Sound)

    @Delete
    suspend fun delete(sound: Sound)
}

DAOインターフェースではSQLのクエリに相当する関数を宣言する.getAllではデータベースのデータを全て取り出し,insertAllでは引数のデータを全てデータベースに保存する.コルーチン内で使用するために各関数にsusupendをつけておく.

Entity

Sound.kt@Entity(tableName = "sounds")
data class Sound(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    @ColumnInfo(name = "file_name") val fileName: String,
    @ColumnInfo(name = "string_uri") val stringUri: String
)

Entityクラスではデータベースに保存するデータの形式をdata classとして定義する.今回音楽ファイルのIDとファイル名とURIを保存する.@PrimaryKey(autoGenerate = true)とすることでIDが自動で生成される.

読み込みと書き込み

データベースの読み込み,書き込みは以下のようにする.

val db = SoundDatabase.getInstance(context)
val dao = db.soundDao()
// 読み込み
CoroutineScope(Dispatchers.Main).launch {
    withContext(Dispatchers.Main) {
        val sounds = soundDao.getAll()
    }
}
// 書き込み
CoroutineScope(Dispatchers.Main).launch {
    withContext(Dispatchers.Main) {
        soundDao.insertAll(sound)
    }
}

データベース操作は基本的にメインスレッド以外で非同期処理で行う必要があるのでコルーチンを用いて操作している.