Roomを勉強したくて、やっぱり最初はCodeLabsだろうということで、Android Room with a View - Kotlinを見ていきます。
CodeLabsの日本語版は全く充実してないので、英語を何とか読みながらやりましたが、需要があるかなということで日本語で簡単に解説していきます。訳は大いに意訳です。
もはやJavaには戻れませんのでKotlin版です。
日本語訳以外に、個人的に嵌まったところ(CodeLab内で触れていなくて罠になっているところ)も覚え書きしていきます。
斜体部分が、個人的な感想、メモ、意見、独り言です。
対象者
- Java,Kotlinを読める
- SQLite3が何か知っていて、簡単なSQLが書ける
- Android Architecture Components(以下AAC)のViewModel 、LiveDataについて何となく知っている
- 非同期処理についてだいたい理解している(MainThreadとWorkerThreadの違いが分かる)
Roomとは
Googleが用意したSQLite3のラッパーみたいな物。AACの一部と捉えて良いと思います。
売りは以下の点だと思っています。
- 非同期処理にデフォルトで対応(同期的にすることも一応可能)
- LiveData(AAC)に対応
1は、基本的にIOはWokerThreadでというポリシーですね。
2が一番胆な所だと思っています。LiveDataにすると、いろいろと楽になってライフサイクル問題も解決できて素晴らしいのですが、ここでは解説しません。LiveDataのことを全く知らないという方は、Android lifecycle-aware components codelabを先にやった方が良いかも知れません。
CodeLab説明
1. Introduction(紹介)
What are the recommended Architecture Components?(AACの何を使うの?)
AACの図を見て、各コンポーネント層が何を提供し、どう関連しあっているのかイメージを掴んで下さい。
- Entity: databaseのtableを定義するクラス。アノテーションを付ける
- SQLite database: データを永続保存するデータベース。Roomが良きに面倒を見てくれる
- DAO: Data access objectの略。データベースのクエリーをマッピングする。メソッドを作ればあとはRoomが良きに面倒を見てくれる
-
Room database: かつては
SQLiteOpenHelper
を使ってアレコレしていたのを面倒見る。DAOを使ってSQLiteにクエリーを発行する - Repository: あなたが作るクラス。複数のデータソースを取り扱う"口"
- ViewModel: データをViewに提供する(MVVM)。UIとRepositoryの間を取り持つ
-
LiveData: observe(観察、監視)されるデータホルダー。データが変わるとオブザーバーに通知される。ライフサイクルを考慮してくれるので、
IllegalStateException
を心配する必要が無い
What you will build(何が出来るの)
Android Architecture Componentsを使ったアプリをビルドします。
サンプルアプリは、単語をRoomデータベースに保存してRecyclerView
で表示します。
サンプルは必要最低限のものですが、アプリのテンプレートとして使用できるくらい充分に複雑です。
サンプルアプリは以下の機能を持ちます。
- データベースにデータを保存して取得する。データベースは初期データを持つ
- データベースに登録された全単語を
MainActivity
のRecyclerView
で表示する - +ボタンで開くSecondActivityで単語を登録する。ユーザーが単語を入力すると、データベースに保存され、リストに追加される
RoomWordSample architecture overview(RoomWordSampleの概要)
RoomWordSample(今回作成するサンプル)のクラス相関図です。
図の説明はしません。データの流れをよく見ておいて下さい。
What you'll learn(何が学べるの)
- AACの Room と Lifecycles ライブラリを使ったアプリをデザインし、作成する方法。
AACと使用されるライブラリの理解にはたくさんのステップが必要です。大事なのは、何が行われているのかを把握し、それぞれのクラスがどのようにフィットしてデータが流れていくかを理解することです。
ただコードをコピペするのでは無く、理解を進めながらビルドして下さい。
What you'll need(必要環境)
- Android Studio3.0以上
- 実機デバイスかエミュレーター
Kotlin、オブジェクト指向が分かることと、下記のようなAndroid開発の基礎を知っていることが必要です。
-
RecyclerView
とadapters - SQLite databaseとSQLiteクエリー言語
- coroutine。知らなければ
Using Kotlin Coroutines in your Android Appで学んで下さい。 (※個人的にはcoroutineも一緒に学べるサンプルだと思います) - データとUIを分離する設計、例えばMVPやMVC等を知っていると理解の助けになる(今回使うのはMVVMです)
このCodeLabはAACにフォーカスしています。関係の無いところはサクッとコピペしていきましょう。
また、このCodeLabはアプリをビルドするすべてコードを提供します。
(ビルドできない、実行できない、動作がおかしいのは、こっちのせいじゃないよ)と言いたいのでしょうが、実際には、そのまま写経(またはコピペ)しているとビルドできないか、クラッシュします(2019/04/27現在)
2. Create your app(アプリを作ろう)
図の通りにプロジェクトを作成します。
設定の説明が分かりづらいのでウィザードの順に箇条書きに変更した記述にしています。
- Phone&Tabletのタブを選択(デフォルトでなっているはず)
- Basic Activityを選択
[次へ]
- アプリ名: RoomWordSample
- パッケージ名: 任意でどうぞ
- Language: Kotlin(デフォルトがJavaになっているので要変更)
- Minimum API Level: 持っている端末に合わせて
- Use AndroidX artifacts: チェックする(デフォルトがオフになっているので要変更)
[Finish]
※AndroidXのところが触れられてないので、個人的にはハマりポイント1でした。
3. Update gradle files(gradleファイルの更新)
必要なライブラリの依存をbuild.gradleに追加します。
app/build.gradleの先頭に下記の行を追加します。
apply plugin: 'kotlin-kapt'
同じファイルのdependenciesに下記を追加します。
※個人的にバージョンが外出ししてあるのは嫌いなので直書きに変更しています。
// Room components
implementation "android.arch.persistence.room:runtime:1.1.1"
kapt "android.arch.persistence.room:compiler:1.1.1"
androidTestImplementation "android.arch.persistence.room:testing:1.1.1"
// Lifecycle components
implementation "android.arch.lifecycle:extensions:1.1.1"
kapt "android.arch.lifecycle:compiler:1.1.1"
// Coroutines
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.0"
coroutineのexperimental指定は不要です。(公式バージョンになりました)
4.Create the entity(entityを作る)
このアプリの「データ」は「単語」で、各単語がエンティティとなります。
まず始めにWordデータクラスを作ります。
data class Word(val word: String)
次に、Room向けにテーブル定義であることをアノテーションで指定します。
-
@Entity(tableName = "word_table"):
テーブルネームが"word_table"のEntityであることを示すアノテーション -
@PrimaryKey
テーブルのプライマリーキーを示すアノテーション -
@ColumnInfo(name = "word")
カラム定義を示すアノテーション。
(※変数名と同じであっても省略できません。Gson等に慣れていると省略したくなりますが、してはダメ。ハマりポイント2でした)
アノテーションのすべてはRoom package summary referenceで見られます。
対応したWordクラスは次のようになります。
@Entity(tableName = "word_table")
data class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)
※CodeLabのコードはdata class
じゃ無くなってますのが、これは間違いです。data class
にして下さい。
5.Create the DAO(DAOを作る)
What is the DAO?(DAOって何?)
アノテーションの付いたDAOクラスを作成すると、コンパイラーがSQL文をよしなに作成してくれます。
DAOはインターフェースか、抽象クラスである必要があります。
基本的には、すべてのクエリーは別スレッドで動く必要があります。(すべて非同期処理になるということです。)
Implement the DAO(DAOを実装)
このCodeLabでのDAOは、すべての単語を取得する、単語を挿入する、単語を削除する、というクエリーを提供します。
-
WordDao
という名のinterface
を作成する -
RoomにDAOだと認識させるために、
@Dao
というアノテーションを付ける -
単語を一つ挿入するメソッドを定義する:
fun insert (word: Word)
-
そのメソッドに
@Insert
アノテーションを付ける。SQL文を自分で書く必要はありません! (他に@Delete
や@Update
というアノテーションもありますが、このアプリでは使いません) -
すべての単語を削除するメソッドを定義する:
fun deleteAll()
-
複数のデータを削除出来るアノテーションは無いので、一般的な
@Query
アノテーションを使う -
@Query
アノテーションにSQL文を文字列で指定する
@Query("DELETE FROM word_table")
※この辺はイケてない気がしますね。@DeleteAll
くらい用意して欲しいなー。テーブル名のtypoとかが怖い。 -
すべての単語を
Word
のList
で取得するメソッドを定義する
fun get getAllWords():List<Word>
-
そのメソッドの
@Query
アノテーションにクエリーを指定する
@Query("SELECT * from word_table ORDER BY word ASC")
Daoの全コードは次の通りです。
@Dao
interface WordDao{
@Insert
fun insert(word:Word)
@Query("DELETE FROM word_table")
fun deleteAll()
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAllWords():List<Word>
}
6.The LiveData class(LiveDataクラス)
データが更新されたとき、だいたい画面を更新したりしたいものです。そのデータをObserve(観察、監視)していれば、変更に対して反応することが出来ます。データがどう保存されているかによって、その実現方法は実にトリッキーになることもあります。アプリの複数のコンポーネントにわたってデータの変更を監視すると、コンポーネント間に複雑な依存関係が作成される可能性があります。そしてテストやデバッグを非常に面倒なものにします。
lifecycleライブラリのLiveDataは、これを解決します。LiveDataを返すように定義すると、Roomがデータの変更を通知するように内部的にコードを生成してくれます。
WordDao
インターフェースの、getAllWords()
メソッドの戻り値をLiveData
でラップしたList<Word>
を返すように変更します。
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAllWords(): LiveData<List<Word>>
このLiveDataの観察コードは、後でやりますが、概要としては、MainActivity
のonCreate
でObserveオブジェクトを作成します。データの更新があるとonChanged
メソッドが呼ばれるので、その中でデータの更新に対するUIのアップデートを行います。
7. Add a Room database(Room databaseの追加)
What is a Room database?(Room databaseって何?)
- RoomはSQLite databaseのトップに位置するレイヤー
- RoomはSQLiteOpenHelperで処理していたタスクを処理する
- Roomはdatabseのクエリーを発行するのにDAOを利用する
- UIのパフォーマンス低下を避けるため、基本的には非同期で実行(メインスレッドでの実行は基本的に許可されていない)。LiveDataを返すように実装していると、自動的にバックグラウンドで実行される
- SQLite文のチェックはコンパイル時に行われる
Implement the Room database(Room databaseの実装)
RoomDatabase
を継承した抽象クラスを作成します。Room databaseはアプリ全体で一つのインスタンスを保持します。
-
RoomDatabase
を継承した抽象クラスWordRoomDatabase
を作成するabstract class WordRoomDatabase:RoomDatabase(){} ```
-
Room Databaseであることを示すアノテーションを付ける。パラメータには、含まれる Entityの配列、バージョン番号をセットする。Entityの配列の指定により、対応するテーブルが作成される
@Database(entities = [Word::class], version = 1)
※CodeLabのページは
entities = {Word.class}
となっていて間違いです。Java向けの記述が中途半端に残ってる感じですね。 -
Daoを返すgetter抽象メソッドを作成する。
@Dao
を付与して作成したDaoクラスのgetterをすべて作成するabstract fun wordDao(): WordDao
ここまでのコードは次の通りです。
@Database(entities = [Word::class], version = 1) abstract class WordRoomDatabase:RoomDatabase(){ abstract fun wordDao(): WordDao }
-
WordRoomDatabaseをシングルトンにする。複数インスタンスがあるのを許容すると、databaseが同時に開かれてしまうことになるので、それを回避するため。
シングルトンにするコードはこちら。
companion object { @Volatile private var INSTANCE: WordRoomDatabase? = null fun getDatabase(context: Context): WordRoomDatabase { return INSTANCE ?: synchronized(this) { // Create database here val instance = // TODO INSTANCE = instance instance } } }
-
RoomDatabaseのbuilderを使い、Room databaseを取得するコードを実装する。アプリケーションコンテキストを使い、データベース名は"Word_database"とする
val instance = Room.databaseBuilder( context.applicationContext, WordRoomDatabase::class.java, "Word_database"
).build()
```
全体のコードは次のようになります。
@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 {
return INSTANCE ?: synchronized(this) {
// Create database here
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"Word_database"
).build()
INSTANCE = instance
instance
}
}
}
}
※CodeLabに記載の全体コードでは、エルビス演算子(?:
)を使わない書き方に変わってますが、個人的にはそのまま残したいのでそちらを転記しています。だってその方がkotlinらしいじゃん??
databaseのスキーマを変更したときは、versionをアップしていきます。databaseのマイグレーションについては、Understanding migrations with Roomを見て下さい。
ビルドでエラーが出たら、 Build >Clean Projectしてから、Build > Rebuild Projectしてみて下さい。でも、ビルドしてエラーが出たらあなたのコードがおかしいんだよ。全部出してるんだから。
いやいや、そのままだと普通にエラー出るよw {}
の件とか。META_INFの件とか。
※下記ビルドエラーが出る場合
More than one file was found with OS independent path 'META-INF/<module_name>'
app/build.gradleに下記を追記して下さい。
android{
...
packagingOptions {
exclude 'META-INF/<module_name>'
}
}
エラーが出た数だけ追記して下さい。
ここまでで一度実行を確認しておきましょう。
8. Create the Repository(リポジトリを作る)
リポジトリっていってもソース管理のリポジトリでは無い。
What is a Repository?(リポジトリって何?)
リポジトリクラスは、複数のデータソースにアクセスする手段を抽象化します。これはAACの一部では無いですが、コードの分離とAACのベストプラクティスとして提唱されています。リポジトリクラスはアプリケーション全体にデータアクセスへのAPIを提供するのが目的です。
Why use a Repository?(なんでリポジトリを使うの?)
データをネットワーク経由で取ってくるのか、ローカルのデータベースから取ってくるのか等、リポジトリクラスがデータのバックグラウンドについて管理するため、UI層からはそれらを意識する必要がなくなります。(テストの分離がしやすくなります)
Implementing the Repository(リポジトリの実装)
-
WordRepository
クラスを作成する -
DAOをprivateプロパティとしてコンストラクタで受け取る
class WordRepository(private val wordDao: WordDao) { }
-
publicプロパティとしてall wordリストを定義し、初期化する。変更の通知を受け取るため、LiveDataにする
この説明の行重複してるよねval allWords: LiveData<List<Word>> = wordDao.getAllWords()
-
insert()
メソッドをラップする。非同期にWorkerThreadですべてのクエリーは行われる必要があるため、@WorkerThread
アノテーションを付ける。また、coroutine向けであることをコンパイラーに明示するため、suspend
修飾子を付ける。@WorkerThread suspend fun insert(word: Word) { wordDao.insert(word) }
リポジトリクラスの全体のコードは次の通りです。
class WordRepository(private val wordDao: WordDao){
val allWords: LiveData<List<Word>> = wordDao.getAllWords()
@WorkerThread
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}
9. Create the ViewModel(ViewModelを作る)
What is a ViewModel?(ViewModelって何?)
ViewModel
の役割は、UIへデータの提供をすることと、それをコンフィグ変更*(※画面回転などのActicity再作成を指すと思われます)でも維持することです。ViewModel
はリポジトリとUIの間で橋渡しを行います。また、Fragmentとのデータ共有にも使えます(Activity間での共有には使えません。うまくシングルトンにすればいいかも知れませんが、一筋縄ではいかないようです。素直にIntent等で共有しましょう。リポジトリがあれば、それぞれのViewModelでリポジトリを参照すれば良いのですし。リポジトリはシングルトンに出来ますが、それも副作用がある場合があるので注意してください)*。ViewModel
はAACのlifcycle libraryの一部です。
Why use a ViewModel?(なぜViewModelを使うの?)
ViewModelは、UIが表示するデータをライフサイクルを意識した方法で保持し、画面のコンフィグ変更でも生き残るようにします。データをActivity
やFragment
から分離することで、それぞれの役割、責任をシンプルにすることが出来ます。例えば、ViewModelはデータの保持・管理に責任を持ち、ActivityやFragmentはその表示だけに責任をもつ、というようにです。
ViewModel
クラスでは、LiveData
を使いましょう。利点は次の通りです。
- observerをデータにセットすれば、本当にデータが変更された時にのみ表示を更新できる
- UIとリポジトリは
ViewModel
によって完全に分離される。ViewModelから直接databaseにアクセスすることは無い。これにより、UnitTestで独立したテストが出来るようになる
Implement the ViewModel(ViewModelの実装)
-
WordViewModel
クラスを作成する。Application
をコンストラクタで受け取り、AndroidViewModel
を継承するclass WordViewModel(app: Application):AndroidViewModel(app) {
}
```
-
privateでリポジトリクラスのメンバーを宣言する
private val repository: WordRepository
※この時点では、ビルドエラーになりますが気にせず先に進みます
-
LiveDataのメンバーを宣言する。このメンバーはデータベースに登録された全単語を持つリストである
val allWords: LiveData<List<Word>>
private
とCodeLabには書いてありますが、嘘です。publicです。※この時点では、ビルドエラーになりますが気にせず先に進みます
-
init
ブロックでWordDao
のインスタンスをWordRoomDatabase
から取得する。そのwordDaoを元に、リポジトリクラスのインスタンスを作成するinit { val wordsDao = WordRoomDatabase.getDatabase(application).wordDao() repository = WordRepository(wordsDao) }
-
同じく
init
ブロックで、allWordsを初期化するallWords = repository.allWords
-
parentJob
とcoroutineContext
を宣言する。coroutineContext
はparentJob
とMain Dispatcherを使う。このcoroutineContext
を使ってCoroutineScope
のインスタンスをnewするprivate var parentJob = Job() private val coroutineContext: CoroutineContext get() = parentJob + Dispatchers.Main private val scope = CoroutineScope(coroutineContext)
※
CoroutineContext
をimportするときに、experimental
の方をimportしないように注意してください。 -
ViewModelクラスの
onCleared
をオーバーライドして、そのなかでparentJobをキャンセルするoverride fun onCleared() { super.onCleared() parentJob.cancel() }
-
insert()
メソッドを作成してリポジトリクラスのinsert()
メソッドを呼び出す。insert()
は非同期でメインスレッド以外で呼ばれる必要があるため、IO Dispatcher
でcoroutineを起動するfun insert(word: Word) = scope.launch(Dispatchers.IO){ repository.insert(word) }
WordViewModel
の全コードは以下の通りです。
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 wordDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordDao)
allWords = repository.allWords
}
override fun onCleared() {
super.onCleared()
parentJob.cancel()
}
fun insert(word: Word) = scope.launch(Dispatchers.IO){
repository.insert(word)
}
}
注意: ViewModelにContextを保持しないでください。メモリリークする原因になります。Contextが必要な場合は、AndroidViewModelクラスで受け取れるアプリケーションコンテキストを利用します。
アプリケーションコンテキストが不要な場合は、ViewModel
というクラスを継承して作成します
重要: ViewModelは、onSavedInstance
を不要にはしません。なぜなら、ViewModelは、プロセスのシャットダウンを経ては生き残りません。
ViewModelのデータが生き残るのは、画面回転したとき程度のようです。Acitivityが裏に行って破棄される、いわゆる「Activityを保持しない」設定で疑似再現できる状態においては、ViewModelクラスも削除されます。
10. Add XML layout(layout xmlを追加する)
MainActivityのレイアウトを作成します。
values/styles.xml
に下記を追記します。
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">26dp</item>
<item name="android:textSize">24sp</item>
<item name="android:textStyle">bold</item>
<item name="android:layout_marginBottom">6dp</item>
<item name="android:paddingLeft">8dp</item>
</style>
layout/recyclerview_item.xml
を追加し、下記のように記載します。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>
layout/content_main.xml
の中の, TextView
をRecyclerView
に置き換えます。
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
tools:listitem="@layout/recyclerview_item" />
※ここが個人的に最大のハマりポイントでした。CodeLabではsupport.v7
のRecyclerViewを指定していますが、これを使うには当然com.android.support:recyclerview-v7
をdependenciesに書かなければなりません。が、support library
への依存については、CodeLabの記事全体で触れられていません。コピペだけしていると、この時点でアプリを実行した時にクラッシュします。support library
を自分でbuild.gradle
に追記するか、もしくはjecpack(androix)
を使う必要が有り、今回は後者を取りました。
Floating Action Button(Fab)の画像を変更します。
-
File > New > Vector Assetと選択
-
ClipArt:
のドロイド君アイコンが表示されているところをクリックし、検索窓に"add"と入力、十字のadd
アイコンを選択 -
Fabの画像設定を以下のように変更する
android:src="@drawable/ic_add_black_24dp"
※元の
app:srcCompat=
の行は削除し、上の行を追加します
11. Add a RecyclerView(RecyclerViewを追加)
RecyclerView
でデータを表示します。RecyclerView
やAdapter
の使い方については知っているものとします。ここのコードはコピペしても構いません。
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}
MainActivity#onCreate
に以下を追記します。
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
実行してみよう!
Adapterにデータをセットしていないのでリストはまだ表示されません。
12. Populate the database(データベースにデータを投入する)
まだdatabaseにデータが無いので、オープンされたときに初期データを投入しておきます。また、データ追加用のActivityも追加します。
アプリケーションが開始したときに、全データを削除して初期データを投入するには、RoomDatabase.Callback
を作ってonOpen()
をオーバーライドします。これもUIスレッドでは実行できないので、coroutineのIO Dispatcher scopeで起動します。
それをするには、getDatabase
にCoroutineScope
が必要となります。よって以下のようにWordRoomDatabase
のgetDatabase
を変更します。
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}
呼び出し元も変更します。
val wordDao = WordRoomDatabase.getDatabase(application, scope).wordDao()
WordRoomDatabase
クラス内に、RoomDatabase.Callback()
を実装するカスタムクラスをインナークラスで定義します。コンストラクタでscopeを受け取り、onOpen()
をオーバーライドします。
ここまでのコードは以下のようになります。
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())
}
}
}
}
メソッドpopulateDatabase
を作成します。データを一度すべて削除してから、好きな単語をinsertしておきます。
fun populateDatabase(wordDao: WordDao) {
wordDao.deleteAll()
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
}
作成したcallbackを、databaseのbuildシーケンスに追加します。addCallback
を、.build()
の前に記述します。
.addCallback(WordDatabaseCallback(scope)
WordRoomDatabase
のgetDatabase
はこうなります。
fun getDatabase(context: Context, scope: CoroutineScope): WordRoomDatabase {
return INSTANCE ?: synchronized(this) {
// Create database here
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"Word_database"
).addCallback(WordDatabaseCallback(scope)
).build()
INSTANCE = instance
instance
}
}
13. Add NewWordActivity(NewWordActivityを追加)
単語を登録するためのAcivitiyを追加します。
標準的なAndroidのActivity作成のため、詳しい説明は割愛します。
NewWordActivityの作成は、File>New>Activity>EmptyActivityで作成するのが良いです。Manifestファイルへの追記漏れを防げます。
先にレイアウトファイル(activity_new_word.xml
)を作って下さい。
NewWordActivityのコードは次のようになります。
class NewWordActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
button_save.setOnClickListener{
val replyIntent = Intent()
if (TextUtils.isEmpty(edit_word.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = edit_word.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
※kotlin-extensionsを使った書き方に変更しています。
14. Connect with the data(データを接続する)
やっと最終ステップです。RecyclerView
にユーザーが追加した物を含め保存された単語をリスト表示します。
データベースの最新の情報を表示するには、ViewModel
のLiveData
をobserveします。データの変更がある度に、onChanged
が呼ばれるので、その中でadapter
にsetWord
すればリストが更新されます。
MainActivity
にViewModel
メンバーを作成します。
private lateinit var wordViewModel: WordViewModel
onCreate
メソッドでViewModelのインスタンスを取得します。
wordViewModel = ViewModelProviders.of(this).get(WordViewModel::class.java)
同じくonCreate
の中で、allWords
で返されるLiveData
を監視するコードを追加します。(※CodeLabの記述ではgetAllWords
になってますがこれはJavaのメソッド名ですね。Kotlinなのでpropertyアクセスにしましょう。)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
MainActivity
にNewWordActivity
を起動するときのリクエストコードを定義します。
companion object {
const val newWordActivityRequestCode = 1
}
MainActivity
のonActivityResult
をオーバーライドし、NewWordActivity
からの結果を受け取ります。resultCode
がRESULT_OK
のとき、ViewModel#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()
}
}
最後に、Fabボタンが押されたときのコードを変更し、NewWordActivity
が開くようにします。
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
実行してみよう!!
起動すれば初期値が表示されているのが分かります。
+ボタンを押して、単語を追加すると、リストも追加されています。
15. Summary(要約)
おさらいなので、割愛します。
16. Further explorations(更に知りたい人は)
発展的内容なので割愛します。
最後に
感想
coroutineも入ってきたのでちょっと難しく感じましたが、SQLiteOpenHelper
を使ってゴリゴリ書いたり、Daoを生成する外部ライブラリを使ったりするより、ずっと直感的でシンプルに書けて非常に使い勝手が良さそうです。
SQLインジェクションなんかも気にする必要があまりなくなりますね。
(とはいえクエリー文を直に書かないと行けないような全データ削除、などもありますが)
1点だけ、基本的にRoomは非同期でやることが推奨されていますが、マイグレーションするときなどで、既存コードを非同期の書き方に直すのが大変なプロジェクトなどでは、同期的アクセスを許可する方法があるので紹介しておきます。
(補足)Roomで同期的処理を許可する
Room.databaseBuilder(context, SampleDatabase::class.java, DATABASE_NAME)
.allowMainThreadQueries() //MainThreadでの実行を許可します
.build()
こちらで教えて頂きました。
AndroidでRoom+RxJavaを実装する