LoginSignup
23
19

More than 5 years have passed since last update.

Google CodeLabs Android Room with a View - Kotlinを日本語で概要解説

Posted at

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の一部と捉えて良いと思います。
売りは以下の点だと思っています。

  1. 非同期処理にデフォルトで対応(同期的にすることも一応可能)
  2. 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で表示します。
サンプルは必要最低限のものですが、アプリのテンプレートとして使用できるくらい充分に複雑です。

サンプルアプリは以下の機能を持ちます。

  • データベースにデータを保存して取得する。データベースは初期データを持つ
  • データベースに登録された全単語をMainActivityRecyclerViewで表示する
  • +ボタンで開く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はアプリをビルドするすべてコードを提供します。

(ビルドできない、実行できない、動作がおかしいのは、こっちのせいじゃないよ)と言いたいのでしょうが、実際には、そのまま写経(またはコピペ)しているとビルドできないか、クラッシュします:sweat:(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の先頭に下記の行を追加します。

app/build.gradle
apply plugin: 'kotlin-kapt'

同じファイルのdependenciesに下記を追加します。
※個人的にバージョンが外出ししてあるのは嫌いなので直書きに変更しています。

app/build.gradle
    // 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でした:sweat:)

アノテーションのすべては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は、すべての単語を取得する、単語を挿入する、単語を削除する、というクエリーを提供します。

  1. WordDaoという名のinterfaceを作成する
  2. RoomにDAOだと認識させるために、@Daoというアノテーションを付ける
  3. 単語を一つ挿入するメソッドを定義する: fun insert (word: Word)
  4. そのメソッドに@Insertアノテーションを付ける。SQL文を自分で書く必要はありません! (他に@Delete@Updateというアノテーションもありますが、このアプリでは使いません)
  5. すべての単語を削除するメソッドを定義する: fun deleteAll()
  6. 複数のデータを削除出来るアノテーションは無いので、一般的な@Queryアノテーションを使う
  7. @QueryアノテーションにSQL文を文字列で指定する
    @Query("DELETE FROM word_table")
    ※この辺はイケてない気がしますね。@DeleteAllくらい用意して欲しいなー。テーブル名のtypoとかが怖い。

  8. すべての単語をWordListで取得するメソッドを定義する
    fun get getAllWords():List<Word>

  9. そのメソッドの@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の観察コードは、後でやりますが、概要としては、MainActivityonCreateで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はアプリ全体で一つのインスタンスを保持します。

  1. RoomDatabaseを継承した抽象クラスWordRoomDatabaseを作成する

    abstract class WordRoomDatabase:RoomDatabase(){}
    
  2. Room Databaseであることを示すアノテーションを付ける。パラメータには、含まれる Entityの配列、バージョン番号をセットする。Entityの配列の指定により、対応するテーブルが作成される

    @Database(entities = [Word::class], version = 1)
    

    ※CodeLabのページはentities = {Word.class}となっていて間違いです。Java向けの記述が中途半端に残ってる感じですね。

  3. Daoを返すgetter抽象メソッドを作成する。@Daoを付与して作成したDaoクラスのgetterをすべて作成する

    abstract fun wordDao(): WordDao
    

    ここまでのコードは次の通りです。

    @Database(entities = [Word::class], version = 1)
        abstract class WordRoomDatabase:RoomDatabase(){
        abstract fun wordDao(): WordDao
    }
    
  4. 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
            }
        }
    }
    
  5. 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に下記を追記して下さい。

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(リポジトリの実装)

  1. WordRepositoryクラスを作成する
  2. DAOをprivateプロパティとしてコンストラクタで受け取る

    class WordRepository(private val wordDao: WordDao) {
    }
    
  3. publicプロパティとしてall wordリストを定義し、初期化する。変更の通知を受け取るため、LiveDataにする
    この説明の行重複してるよね

    val allWords: LiveData<List<Word>> = wordDao.getAllWords()
    
  4. 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が表示するデータをライフサイクルを意識した方法で保持し、画面のコンフィグ変更でも生き残るようにします。データをActivityFragmentから分離することで、それぞれの役割、責任をシンプルにすることが出来ます。例えば、ViewModelはデータの保持・管理に責任を持ち、ActivityやFragmentはその表示だけに責任をもつ、というようにです。

ViewModelクラスでは、LiveDataを使いましょう。利点は次の通りです。

  • observerをデータにセットすれば、本当にデータが変更された時にのみ表示を更新できる
  • UIとリポジトリはViewModelによって完全に分離される。ViewModelから直接databaseにアクセスすることは無い。これにより、UnitTestで独立したテストが出来るようになる

Implement the ViewModel(ViewModelの実装)

  1. WordViewModelクラスを作成する。Applicationをコンストラクタで受け取り、AndroidViewModelを継承する

    class WordViewModel(app: Application):AndroidViewModel(app) {
    }
    
  2. privateでリポジトリクラスのメンバーを宣言する

    private val repository: WordRepository
    

    ※この時点では、ビルドエラーになりますが気にせず先に進みます

  3. LiveDataのメンバーを宣言する。このメンバーはデータベースに登録された全単語を持つリストである

    val allWords: LiveData<List<Word>>
    

    privateとCodeLabには書いてありますが、嘘です。publicです。

    ※この時点では、ビルドエラーになりますが気にせず先に進みます

  4. initブロックでWordDaoのインスタンスをWordRoomDatabaseから取得する。そのwordDaoを元に、リポジトリクラスのインスタンスを作成する

    init {
       val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
       repository = WordRepository(wordsDao)
    }
    
  5. 同じくinitブロックで、allWordsを初期化する

    allWords = repository.allWords
    
  6. parentJobcoroutineContextを宣言する。coroutineContextparentJobと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しないように注意してください。

  7. ViewModelクラスのonClearedをオーバーライドして、そのなかでparentJobをキャンセルする

    override fun onCleared() {
        super.onCleared()
        parentJob.cancel()
    }
    
  8. 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に下記を追記します。

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を追加し、下記のように記載します。

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の中の, TextViewRecyclerViewに置き換えます。

content_main.xml
<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)の画像を変更します。

  1. File > New > Vector Assetと選択
  2. ClipArt:のドロイド君アイコンが表示されているところをクリックし、検索窓に"add"と入力、十字のaddアイコンを選択
  3. Fabの画像設定を以下のように変更する

    android:src="@drawable/ic_add_black_24dp"
    

    ※元のapp:srcCompat=の行は削除し、上の行を追加します

11. Add a RecyclerView(RecyclerViewを追加)

RecyclerViewでデータを表示します。RecyclerViewAdapterの使い方については知っているものとします。ここのコードはコピペしても構いません。

WordListAdapter
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に以下を追記します。

MainActivity.kt
    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で起動します。

それをするには、getDatabaseCoroutineScopeが必要となります。よって以下のようにWordRoomDatabasegetDatabaseを変更します。

WordRoomDatabase
fun getDatabase(
       context: Context,
       scope: CoroutineScope
  ): WordRoomDatabase {
...
}

呼び出し元も変更します。

WordViewModel
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しておきます。

WordDatabaseCallback
fun populateDatabase(wordDao: WordDao) {
     wordDao.deleteAll()

     var word = Word("Hello")
     wordDao.insert(word)
     word = Word("World!")
     wordDao.insert(word)
}

作成したcallbackを、databaseのbuildシーケンスに追加します。addCallbackを、.build()の前に記述します。

WordRoomDatabase
.addCallback(WordDatabaseCallback(scope)

WordRoomDatabasegetDatabaseはこうなります。

WordRoomDatabase
        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のコードは次のようになります。

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にユーザーが追加した物を含め保存された単語をリスト表示します。

データベースの最新の情報を表示するには、ViewModelLiveDataをobserveします。データの変更がある度に、onChangedが呼ばれるので、その中でadaptersetWordすればリストが更新されます。

MainActivityViewModelメンバーを作成します。

MainActivity
private lateinit var wordViewModel: WordViewModel

onCreateメソッドでViewModelのインスタンスを取得します。

MainActivity
wordViewModel = ViewModelProviders.of(this).get(WordViewModel::class.java)

同じくonCreateの中で、allWordsで返されるLiveDataを監視するコードを追加します。(※CodeLabの記述ではgetAllWordsになってますがこれはJavaのメソッド名ですね。Kotlinなのでpropertyアクセスにしましょう。)

MainActivity
wordViewModel.allWords.observe(this, Observer { words ->
  // Update the cached copy of the words in the adapter.
  words?.let { adapter.setWords(it) }
})

MainActivityNewWordActivityを起動するときのリクエストコードを定義します。

MainActivity
companion object {
    const val newWordActivityRequestCode = 1
}

MainActivityonActivityResultをオーバーライドし、NewWordActivityからの結果を受け取ります。resultCodeRESULT_OKのとき、ViewModel#insertでその単語を追加します。

MainActivity
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が開くようにします。

MainActivity
        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を実装する

23
19
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
23
19