Android Architecture Components: Room の再復習
背景
- 急遽RoomとPagingの再復習をする必要に駆られたので頑張る
- まずはRoomから
Room
RoomはQLiteをラップして利用できるコンポーネント。
以下のCodeLabsを行う。 codelabs.developers.google.com
Gradleの設定
以下の設定をbuild.gradleに記載する
// Room components implementation "android.arch.persistence.room:runtime:$rootProject.roomVersion" kapt "android.arch.persistence.room:compiler:$rootProject.roomVersion" androidTestImplementation "android.arch.persistence.room:testing:$rootProject.roomVersion" // Lifecycle components implementation "android.arch.lifecycle:extensions:$rootProject.archLifecycleVersion" kapt "android.arch.lifecycle:compiler:$rootProject.archLifecycleVersion" // Coroutines api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines" api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
build.gradleに記載するimplementationについて復習しておく。
compile : 依存関係を追加する。ただしBプロジェクトをcompileで指定したAプロジェクトは、BがCライブラリに依存していた場合、AもCライブラリに依存することになる。deplicated。 implementation : 依存関係を追加する。ただしBプロジェクトをcompileで指定したAプロジェクトは、BがCライブラリに依存していた場合でもAはCライブラリに依存しない(AはCライブラリを利用できない) api : compileと同じ動作をする。
なぜcoroutinesのdependenciesをapiにしているのかは謎だ。。 あとCodeLabsで利用しているバージョンは0.29.1と古いものだったので最新の1.0.1にしておく。
Entityを作る
ここでいうEntityはSQLの文脈。Entityはテーブルの行を表しており、以下のようなクラスにAnnotation Processingで表すことができる。
@Entity(tableName = "word_table") data class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
各要素について記載します。
@Entity
- テーブル内の行を指定するAnnotation。クラスの名前と別にしたい場合はtableNameを指定する
@PrimaryKey
- すべてのエンティティに主キーを設定する必要がある
@ColumnInfo
- メンバー変数の名前と異なる名前にしたい場合はnameで指定する(なくても良い)
注意事項として、全てのフィールドはpublicもしくはgetterを設定する必要があるようです。
Daoを作る
Data Access Objectを作成する。要するにテーブルに対してアクセスするオブジェクト。
@Dao interface WordDao { @Query("SELECT * from word_table ORDER BY word ASC") fun getAllWords(): List<Word> @Insert fun insert(word: Word) @Query("DELETE FROM word_table") fun deleteAll() }
各要素について記載します。
- @Dao
- Data Access Objectであることを指定する。interfaceに対して指定すること。(このinterfaceから自動的にDataAccessObjectを作ってくれる)
- @Query
- このAnnotationを設定したメソッドを実行すると指定したSQLが実行される
- @Insert
- メソッドの引数に指定したEntityを挿入する。競合した場合の振る舞いをonConflictで指定できる。
- 詳細は次のURLを参照: developer.android.com
LiveDataと連携する
データベース変更された際にUIに通知したいよね。RoomはLiveDataを戻り値に設定してUIに通知できるんです。
@Query("SELECT * from word_table ORDER BY word ASC") fun getAllWords(): LiveData<List<Word>> // LiveDataを設定する
RoomDatabaseを作る
RoomはSQLiteデータベースをラッピングする仕組みです。SQLiteOpenHelperのようなSQLiteを利用する時に必要だったタスクを肩代わりしてくれるようです。 Daoを利用してSQLiteにクエリを発行します。またSQLiteステートメントのコンパイル時チェックをしてくれるようです。素敵!
@Database(entities = [Word::class], version = 1) abstract class WordRoomDatabase : RoomDatabase() { abstract fun wordDao(): WordDao companion object { @Volatile private var INSTANCE: WordRoomDatabase? = null fun getDatabase(context: Context): WordRoomDatabase { val tempInstance = INSTANCE if (tempInstance != null) { return tempInstance } synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, WordRoomDatabase::class.java, "Word_database" // テーブル名 ).build() INSTANCE = instance return instance } } } }
各要素について記載します。
- @Database(entities = [Word::class], version = 1)
- 対象のクラスがDatabaseであることを指定する。
- entitiesにこのDatabaseのテーブルを指定する.
- versionも指定できます。マイグレーション時に必要。
- Databaseに指定するクラスはabstractを指定すること。
- また、このデータベースにアクセスするDaoもabstractメソッドで指定すること。
- @Volatile
- Databaseは1アプリにつき1インスタンスなのでシングルトンとします。
- インスタンスを生成する時はRoom.databaseBuilder()を利用します。
リポジトリを作成する
リポジトリパターンのためにリポジトリを作成します。 基本的にはこの層でAPIから取ってくるかDatabaseから取ってくるかを決める感じです。
class WordRepository(private val wordDao: WordDao) { val allWords: LiveData<List<Word>> = wordDao.getAllWords() @WorkerThread suspend fun insert(word: Word) { wordDao.insert(word) } }
@WorkerThreadは「このメソッドは非同期通信する必要があるよ」ということを明示的に示すためのAnnotaionです。 Roomは非同期(UIThread以外)で利用する必要があるので、記載します。
ViewModel を作る
class WordViewModel(application: Application) : AndroidViewModel(application) { private var parentJob = Job() private val coroutineContext: CoroutineContext get() = parentJob + Dispatchers.Main private val scope = CoroutineScope(coroutineContext) private val repository: WordRepository val allWords: LiveData<List<Word>> init { val wordsDao = WordRoomDatabase.getDatabase(application, scope).wordDao() repository = WordRepository(wordsDao) allWords = repository.allWords } fun insert(word: Word) = scope.launch(Dispatchers.IO) { repository.insert(word) } override fun onCleared() { super.onCleared() parentJob.cancel() } }
ここはViewModelなので省きます。allWordsにLiveDataを設定し、initブロックでRoomと接続してますね。
Databaseにオープン時のCallbackを設定する
Databaseのイベントに対してCallbackを設定できるようです。 ここではオープン時に今までのデータを全て消して二つの単語を登録しています。
@Database(entities = [Word::class], version = 1) abstract class WordRoomDatabase : RoomDatabase() { abstract fun wordDao(): WordDao companion object { @Volatile private var INSTANCE: WordRoomDatabase? = null fun getDatabase( context: Context, scope: CoroutineScope ): WordRoomDatabase { val tempInstance = INSTANCE if (tempInstance != null) { return tempInstance } synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, WordRoomDatabase::class.java, "Word_database" ).addCallback(WordDatabaseCallback(scope)).build() // オープン時のコールバックを設定 INSTANCE = instance return instance } } private class WordDatabaseCallback( private val scope: CoroutineScope ) : RoomDatabase.Callback() { override fun onOpen(db: SupportSQLiteDatabase) { // オープンイベント super.onOpen(db) INSTANCE?.let { database -> scope.launch(Dispatchers.IO) { populateDatabase(database.wordDao()) } } } // オープン時の処理 fun populateDatabase(wordDao: WordDao) { wordDao.deleteAll() var word = Word("Hello") wordDao.insert(word) word = Word("World!") wordDao.insert(word) } } } }
あとはinsertしたり、Observeしたり。
あとはViewModelのLiveDataをObserveして、変更が発生した場合の処理書いたり、
wordViewModel.allWords.observe(this, Observer { words -> // Update the cached copy of the words in the adapter. words?.let { adapter.setWords(it) } })
Roomにデータを追加したい時はinsertしたりできます。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) { data?.let { val word = Word(it.getStringExtra(NewWordActivity.EXTRA_REPLY)) wordViewModel.insert(word) } } else { Toast.makeText( applicationContext, R.string.empty_not_saved, Toast.LENGTH_LONG).show() } }