Android Kotlin基礎講座 06.2: コルーチンとRoom
目次
タスク:データを収集・表示する
以下の方法でユーザーが睡眠データを扱えるようにします。
- ユーザーがStartボタンをタップすると、アプリが新規睡眠データを作成し、データベースに保存する。
- Stopボタンをタップすると、アプリは睡眠データの終了時間を更新する。
- Clearボタンをタップすると、アプリはデータベースのデータを消去する。
これらのデータベース操作には時間がかかる場合があるので、別スレッドで行われるべきです。
ステップ1:DAO関数をsuspend関数にする
SleepDatabaseDao.kt内の関数をsuspend関数に変更していきます。
- 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の作成や更新など、どのボタンを押してもデータベース操作のトリガーとなります。ですので、ボタンのクリックハンドラーを実装するためにコルーチンを使っていきます。
- アプリレベルの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"
- SleepTrackerViewModelファイルを開いてください。
- 今夜のデータを保持するための、tonightという変数を定義してください。この変数はMutableLiveDataにします。これは監視され必要に応じて変更できるようにするためです。
private var tonight = MutableLiveData<SleepNight?>()
- tonight変数をすぐに初期化できるようにするために、tonightの宣言の下にinitブロックを追加し、initializeTonight()を呼び出してください。この関数は次のステップで定義します。
init {
initializeTonight()
}
- initブロックの下に、initializeTonight()関数を実装します。ViewModelScopeのコルーチンを開始させるために、viewModelScope.launchを使います。中括弧の中で、getTonightFromDatabase()を呼び出し、データベースからtonight用の値を取得し、tonight.valueに代入してください。
getTonightFromDatabase()は次のステップで実装します。
private fun initializeTonight() {
viewModelScope.launch {
tonight.value = getTonightFromDatabase()
}
}
- getTonightFromDatabase()を実装します。もし現在スタートしているSleepNightが存在しない場合にnullを許容するSleepNightを返すprivate suspend関数として定義してください。以下のコードはまだ何も値を返していないためエラーが残っています。
private suspend fun getTonightFromDatabase(): SleepNight? { }
- 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()によく似たものになります。
- まずはonStartTracking()関数の定義から始めます。
fun onStartTracking() {}
- onStartTracking()の中で、viewModelScope内のコルーチンを開始させます。UIの継続、更新のためにこの結果が必要なためです。
viewModelScope.launch {}
- コルーチンのlaunchの中で、新規SleepNightを作成してください。これは現在の時間を開始時間として記録します。
val newNight = SleepNight()
- さらにコルーチンのlaunchの中で、newNightをデータベースに挿入するために、insert()を呼び出してください。insert() suspend関数をまだ定義していないので、エラーが発生します。(これは同名のDAO関数ではありません)
insert(newNight)
- さらにlaunch内でtonightを更新してください。
Also inside the coroutine launch, updatetonight
.
tonight.value = getTonightFromDatabase()
- onStartTracking()関数の下に、insert()をprivate suspend関数として定義してください。これはSleepNightを引数にとるようにします。
private suspend fun insert(night: SleepNight) {}
- insert()メソッドの中で、nightをデータベースに挿入するためにDAOを使います。
database.insert(night)
RoomのコルーチンはDispatcher.IOを使っているため、メインスレッドでは行われないことを確認してください。
- fragment_sleep_tracker.xmlレイアウトファイル内に、事前にセットアップしておいたデータバインディングを使ってstart_buttonにonStartTracking()用のクリックハンドラーを追加してください。
@{() ^>という表記は引数を取らずにsleepTrackerViewModelのクリックハンドラーを呼び出すラムダ式を作成しています。.
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
- ビルドしてアプリを起動してください。Startボタンをタップしてください。これによってデータが作成されますが、今のところ何も表示されません。これについては次のステップで修正していきます。
重要: ここでパターンを確認できます。
- 結果がUIに影響を与えるため、メインスレッド、またはUIスレッドで動作するコルーチンを開始させます。以下の例に示されているように、ViewModelのCoroutineScoreにはViewModelのviewModelScopeプロパティを通してアクセスできます。
- ロングランニングムタスクを実行するためにはsuspend関数を呼び出し、結果を待機している間、UIスレッドをブロックしないようにします。
- ロングランニングタスクはUIと関係ありません。I/Oディスパッチャーに切り替えて、ロングランニングタスクが適切かされたスレッドプール内で実行されるようにします。
- その後、タスクを実行するためのロングランニング関数を呼び出します。
パターンは以下に示されている通りです。
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を使います。
- Util.ktファイルを開き、formatNights()の定義とそれに関連するインポート文のコメントアウトを解除してください。Android Studioでコメントアウトを解除するには、//でマークされたコードを全て選択し、Ctrl+/を押します。
- formatNights()がSpanned型を返すことを確認してください。これはHTMLにフォーマットされたstringです。
- strings.xmlを開いてください。睡眠データを表示するためのstringリソースをフォーマットするためにCDATAを使用していることを確認してください。
- SleepTrackerViewModelを開いてください。SleepTrackerViewModelクラスの中で、nightsという変数を定義してください。データベースから全ての夜のデータを取得し、それらをnights変数に代入してください。
private val nights = database.getAllNights()
- nightsの定義のすぐ下に、nightsをnightsStringに変換するためのコードを追加してください。Util.ktからの関数であるformatNights()を使用します。
Transformationsクラスのmap()関数にnightsを渡します。stringリソースにアクセスするために、formatNights()を呼び出すようにmap関数を定義します。nightsとResourcesオブジェクトを渡します。
val nightsString = Transformations.map(nights) { nights ->
formatNights(nights, application.resources)
}
- fragment_sleep_tracker.xmlレイアウトファイルを開いてください。TextViewの中のandroid:textプロパティの中をnightsStringの参照で置き換えることができます。
"@{sleepTrackerViewModel.nightsString}"
- もう一度ビルドして、アプリを起動してください。開始時間と共に全てのデータが表示されています。
- Startボタンを何回か押して、さらにデータを表示できます。
次のステップでは、Stopボタンの機能を実装していきます。
Step 5: Stopボタン用のクリックハンドラーを追加する
前のステップの同じパターンを使って、SleepTrackerViewModelのStopボタン用のクリックハンドラーを実装していきます。
- ViewModelにonStopTracking()を追加してください。ViewModelScope内でコルーチンを開始してください。終了時間がまだセットされていなかった場合は、現在のシステム時間にendTimeMilliを設定し、nightデータを使ってupdate()関数を呼び出します。
Kotlinにおいて、return@構文はいくつかのネストされた関数の中から、この文が返される関数を指定します。
fun onStopTracking() {
viewModelScope.launch {
val oldNight = tonight.value ?: return@launch
oldNight.endTimeMilli = System.currentTimeMillis()
update(oldNight)
}
}
- insert()を実装した時と同じように、update()を実装します。
Implementupdate()
using the same pattern as you used to implementinsert()
.
private suspend fun update(night: SleepNight) {
database.update(night)
}
- クリックハンドラーをUIに接続させるために、fragment_sleep_tracker.xmlレイアウトファイルを開き、stop_buttonにクリックハンドラーを追加してください。
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
- ビルドしてアプリを起動してください。
- Startをタップしたあとに、Stopをタップしてください。開始時間、終了時間、値の無い睡眠の質、睡眠時間が表示されます。
ステップ6:Clearボタン用のクリックハンドラーを追加する
- 同様に、onClear()とclear()を実装していきます。
fun onClear() {
viewModelScope.launch {
clear()
tonight.value = null
}
}
suspend fun clear() {
database.clear()
}
- クリックハンドラーをUIに接続させるために、fragment_sleep_tracker.xmlを開き、clear_buttonにクリックハンドラーを追加してください。
android:onClick="@{() -> sleepTrackerViewModel.onClear()}"
- ビルドしてアプリを起動してください。
- Clearボタンをタップして、データを消去してください。その後StartボタンとStopボタンを押して新しいデータを作成してみてください。
完成済みプロジェクト
お疲れさまでした。完成済みプロジェクトは以下からダウンロードできます。