Android Kotlin基礎講座 06.2: コルーチンとRoom

タスク:データを収集・表示する

以下の方法でユーザーが睡眠データを扱えるようにします。

  • ユーザーがStartボタンをタップすると、アプリが新規睡眠データを作成し、データベースに保存する。
  • Stopボタンをタップすると、アプリは睡眠データの終了時間を更新する。
  • Clearボタンをタップすると、アプリはデータベースのデータを消去する。

これらのデータベース操作には時間がかかる場合があるので、別スレッドで行われるべきです。

ステップ1:DAO関数をsuspend関数にする

SleepDatabaseDao.kt内の関数をsuspend関数に変更していきます。

  1. database/SleepDatabaseDao.ktを開き、getAllNights()を除く全てのメソッドにsuspendキーワードを追加してください。SleepDatabaseDaoクラスは以下のようになります。
@Dao
interface SleepDatabaseDao {

   @Insert
   suspend fun insert(night: SleepNight)

   @Update
   suspend fun update(night: SleepNight)

   @Query("SELECT * from daily_sleep_quality_table WHERE nightId = :key")
   suspend fun get(key: Long): SleepNight?

   @Query("DELETE FROM daily_sleep_quality_table")
   suspend fun clear()

   @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC LIMIT 1")
   suspend fun getTonight(): SleepNight?

   @Query("SELECT * FROM daily_sleep_quality_table ORDER BY nightId DESC")
   fun getAllNights(): LiveData<List<SleepNight>>
}

ステップ2:データベース操作用のコルーチンを設定する

Startボタンがタップされた時、SleepNightのインスタンスをを新規作成し、そのインスタンスをデータベースにに保存するために、SleepTrackerViewModelの関数を呼び出すようにします。

SleepNightの作成や更新など、どのボタンを押してもデータベース操作のトリガーとなります。ですので、ボタンのクリックハンドラーを実装するためにコルーチンを使っていきます。

  1. アプリレベルのbuild.gradleファイルを開いてください。dependenciesセクションのもとに以下の依存関係を追加してください。
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"

// Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
  1. SleepTrackerViewModelファイルを開いてください。
  2. 今夜のデータを保持するための、tonightという変数を定義してください。この変数はMutableLiveDataにします。これは監視され必要に応じて変更できるようにするためです。
private var tonight = MutableLiveData<SleepNight?>()
  1. tonight変数をすぐに初期化できるようにするために、tonightの宣言の下にinitブロックを追加し、initializeTonight()を呼び出してください。この関数は次のステップで定義します。
init {
   initializeTonight()
}
  1. initブロックの下に、initializeTonight()関数を実装します。ViewModelScopeのコルーチンを開始させるために、viewModelScope.launchを使います。中括弧の中で、getTonightFromDatabase()を呼び出し、データベースからtonight用の値を取得し、tonight.valueに代入してください。
    getTonightFromDatabase()は次のステップで実装します。
private fun initializeTonight() {
   viewModelScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. getTonightFromDatabase()を実装します。もし現在スタートしているSleepNightが存在しない場合にnullを許容するSleepNightを返すprivate suspend関数として定義してください。以下のコードはまだ何も値を返していないためエラーが残っています。
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. getTonightFromDatabase関数の中で、データベースからtonight(最新の夜のデータ)を取得します。もし計測開始時間と終了時間が同じでない場合、これはその夜のデータ計測が完了していることを意味するので、nullを返します。そうでない場合、その夜のデータを返します。
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       return night

最終的なgetTonightFromDatabase()関数は以下のようになります。エラーは出ていないはずです。

private suspend fun getTonightFromDatabase(): SleepNight? {
    var night = database.getTonight()
    if (night?.endTimeMilli != night?.startTimeMilli) {
        night = null
    }
    return night
}

ステップ3:Startボタン用のクリックハンドラーを追加する

ここまでで、Startボタン用のクリックハンドラーであるonStartTracking()を実装できる準備が整いました。新規SleepNightを作成し、データベースに挿入、そしてtonightに代入する必要があります。onStartTracking()の構造はinitializeTonight()によく似たものになります。

  1. まずはonStartTracking()関数の定義から始めます。
fun onStartTracking() {}
  1. onStartTracking()の中で、viewModelScope内のコルーチンを開始させます。UIの継続、更新のためにこの結果が必要なためです。
viewModelScope.launch {}
  1. コルーチンのlaunchの中で、新規SleepNightを作成してください。これは現在の時間を開始時間として記録します。
        val newNight = SleepNight()
  1. さらにコルーチンのlaunchの中で、newNightをデータベースに挿入するために、insert()を呼び出してください。insert() suspend関数をまだ定義していないので、エラーが発生します。(これは同名のDAO関数ではありません)
    insert(newNight)
  1. さらにlaunch内でtonightを更新してください。
    Also inside the coroutine launch, update tonight.
       tonight.value = getTonightFromDatabase()
  1. onStartTracking()関数の下に、insert()をprivate suspend関数として定義してください。これはSleepNightを引数にとるようにします。
private suspend fun insert(night: SleepNight) {}
  1. insert()メソッドの中で、nightをデータベースに挿入するためにDAOを使います。
       database.insert(night)

RoomのコルーチンはDispatcher.IOを使っているため、メインスレッドでは行われないことを確認してください。

  1. fragment_sleep_tracker.xmlレイアウトファイル内に、事前にセットアップしておいたデータバインディングを使ってstart_buttonにonStartTracking()用のクリックハンドラーを追加してください。
    @{() ^>という表記は引数を取らずにsleepTrackerViewModelのクリックハンドラーを呼び出すラムダ式を作成しています。.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. ビルドしてアプリを起動してください。Startボタンをタップしてください。これによってデータが作成されますが、今のところ何も表示されません。これについては次のステップで修正していきます。

重要: ここでパターンを確認できます。

  1. 結果がUIに影響を与えるため、メインスレッド、またはUIスレッドで動作するコルーチンを開始させます。以下の例に示されているように、ViewModelのCoroutineScoreにはViewModelのviewModelScopeプロパティを通してアクセスできます。
  2. ロングランニングムタスクを実行するためにはsuspend関数を呼び出し、結果を待機している間、UIスレッドをブロックしないようにします。
  3. ロングランニングタスクはUIと関係ありません。I/Oディスパッチャーに切り替えて、ロングランニングタスクが適切かされたスレッドプール内で実行されるようにします。
  4. その後、タスクを実行するためのロングランニング関数を呼び出します。

パターンは以下に示されている通りです。

fun someWorkNeedsToBeDone {
   viewModelScope.launch {
        suspendFunction()
   }
}

suspend fun suspendFunction() {
   withContext(Dispatchers.IO) {
       longrunningWork()
   }
}

Room使用の場合

// Using Room
fun someWorkNeedsToBeDone {
   viewModelScope.launch {
        suspendDAOFunction()
   }
}

suspend fun suspendDAOFunction() {
   // No need to specify the Dispatcher, Room uses Dispatchers.IO.
   longrunningDatabaseWork()
}

ステップ4:データを表示する

SleepTrackerViewModel内で、nights変数はLiveDataを参照しています。DAOのgetAllNights()関数はLiveDataを返すためです。

データベース内のデータが変更される度、LiveDataのnightsも更新され、最新のデータを表示できるというのはRoomの特色の一つです。明示的にLiveDataを設定したり更新したりする必要はないのです。Roomはデータベースに合致するようにデータを更新してくれます。

しかしながら、テキストビューでnightsを表示した場合、それはオブジェクトの参照を表示します。オブジェクトの内容を表示するためいんは、データをフォーマットされたstringへと変換する必要があります。nightsがデータベースから新しいデータを受け取るたびに実行されるTransformation.mapを使います。

  1. Util.ktファイルを開き、formatNights()の定義とそれに関連するインポート文のコメントアウトを解除してください。Android Studioでコメントアウトを解除するには、//でマークされたコードを全て選択し、Ctrl+/を押します。
  2. formatNights()がSpanned型を返すことを確認してください。これはHTMLにフォーマットされたstringです。
  3. strings.xmlを開いてください。睡眠データを表示するためのstringリソースをフォーマットするためにCDATAを使用していることを確認してください。
  4. SleepTrackerViewModelを開いてください。SleepTrackerViewModelクラスの中で、nightsという変数を定義してください。データベースから全ての夜のデータを取得し、それらをnights変数に代入してください。
private val nights = database.getAllNights()
  1. nightsの定義のすぐ下に、nightsをnightsStringに変換するためのコードを追加してください。Util.ktからの関数であるformatNights()を使用します。

    Transformationsクラスのmap()関数にnightsを渡します。stringリソースにアクセスするために、formatNights()を呼び出すようにmap関数を定義します。nightsとResourcesオブジェクトを渡します。
val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. fragment_sleep_tracker.xmlレイアウトファイルを開いてください。TextViewの中のandroid:textプロパティの中をnightsStringの参照で置き換えることができます。
"@{sleepTrackerViewModel.nightsString}"
  1. もう一度ビルドして、アプリを起動してください。開始時間と共に全てのデータが表示されています。
  2. Startボタンを何回か押して、さらにデータを表示できます。

次のステップでは、Stopボタンの機能を実装していきます。

Step 5: Stopボタン用のクリックハンドラーを追加する

前のステップの同じパターンを使って、SleepTrackerViewModelのStopボタン用のクリックハンドラーを実装していきます。

  1. ViewModelにonStopTracking()を追加してください。ViewModelScope内でコルーチンを開始してください。終了時間がまだセットされていなかった場合は、現在のシステム時間にendTimeMilliを設定し、nightデータを使ってupdate()関数を呼び出します。

    Kotlinにおいて、return@構文はいくつかのネストされた関数の中から、この文が返される関数を指定します。
fun onStopTracking() {
   viewModelScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. insert()を実装した時と同じように、update()を実装します。
    Implement update() using the same pattern as you used to implement insert().
private suspend fun update(night: SleepNight) {
    database.update(night)
}
  1. クリックハンドラーをUIに接続させるために、fragment_sleep_tracker.xmlレイアウトファイルを開き、stop_buttonにクリックハンドラーを追加してください。
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
  1. ビルドしてアプリを起動してください。
  2. Startをタップしたあとに、Stopをタップしてください。開始時間、終了時間、値の無い睡眠の質、睡眠時間が表示されます。

ステップ6:Clearボタン用のクリックハンドラーを追加する

  1. 同様に、onClear()とclear()を実装していきます。
fun onClear() {
   viewModelScope.launch {
       clear()
       tonight.value = null
   }
}

suspend fun clear() {
    database.clear()
}
  1. クリックハンドラーをUIに接続させるために、fragment_sleep_tracker.xmlを開き、clear_buttonにクリックハンドラーを追加してください。
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
  1. ビルドしてアプリを起動してください。
  2. Clearボタンをタップして、データを消去してください。その後StartボタンとStopボタンを押して新しいデータを作成してみてください。

完成済みプロジェクト

お疲れさまでした。完成済みプロジェクトは以下からダウンロードできます。

TrackMySleepQualityCoroutines