iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day20【Coroutines and Room編】

学ぶこと

  • Android でスレッドがどのように機能するか
  • Kotlin coroutines を使用して、database 操作をメインスレッドから移動する方法
  • フォーマットされたデータ TextView に表示する方法

すること

  • TrackMySleepQuality アプリを拡張して、データベースとの間でデータを収集、保存、および表示します。
  • coroutines を使用して、データベース操作をバックグランドで実行します。
  • LiveData を使用して、navigation と snackbar の表示をトリガーします。
  • LiveData を使用して、ボタンを有効または無効にします。

Starter code を調べる

このタスクでは、TextView を使用して、フォーマットされた睡眠追跡データを表示します。

  1. res/layout/activity_main.xml を開きます。このレイアウトには、nav_host_fragment fragment が含まれています。また、<merge> タグにも注目してください。

merge タグは、レイアウトを含める時に冗長なレイアウトを排除するために使用できます。これを使用することが推奨されています。冗長レイアウトの例は、ConstraintLayout > LinearLayout > TextView であり、システムは LinearLayout を排除できる可能性があります。この種の最適化により、View 階層が簡素化され、アプリのパフォーマンスが向上します。

  1. navigation フォルダーで、navigation.xml を開きます。2つの Fragmnet と、それらを接続する navigation action を確認できます。

  2. layout フォルダーで、sleep tracker fragment をダブルクリックして、XML layout を表示します。次の点に集中してください。

  3. layout data は、data binding を有効にするために <layout> 要素でラップされます。

  4. ConstraintLayout およびその他の View は、<layout> 要素内に配置されます。
  5. ファイルにはプレースホルダ<data> タグがあります。

starter app は、dimensions, color, UI style も提供します。アプリには、Room データベース、DAO、および SleepNight Entity が含まれています。

ViewModel を追加する

データベースと UI ができたので、データを収集し、データベースにデータを追加して、データを表示する必要があります。この作業は全て view model で行われます。sleep-tracker view model は、ボタンのクリックを処理し、DAO を介してデータベースと対話し、LiveData を介して UI にデータを提供します。全てのデータベース操作を main UI スレッドから退避させる必要があるため、coroutines を使用します。

SleepTrackerViewModel を追加します

  1. sleeptracker パッケージで、SleepTrackerViewModel.kt を開きます。
  2. スターターアプリで提供され、以下にも示されている SleepTrackerViewModel クラスを調べます。クラスは AndroidViewModel を拡張することに注意してください。このクラスは ViewModel と同じですが、アプリケーションコンテキストをコンストラクターパラメーターとして受け取り、プロパティとして使用できるようにします。これは後で必要になります。
class SleepTrackerViewModel(
       val database: SleepDatabaseDao,
       application: Application) : AndroidViewModel(application) {
}

SleepTrackerViewModelFactory を追加します

  1. sleeptracker パッケージで、SleepTrackerViewModelFactory.kt を開きます。

  2. 以下に示す、ファクトリ用に提供されているいるコードを調べます。

class SleepTrackerViewModelFactory(
       private val dataSource: SleepDatabaseDao,
       private val application: Application) : ViewModelProvider.Factory {
   @Suppress("unchecked_cast")
   override fun <T : ViewModel?> create(modelClass: Class<T>): T {
       if (modelClass.isAssignableFrom(SleepTrackerViewModel::class.java)) {
           return SleepTrackerViewModel(dataSource, application) as T
       }
       throw IllegalArgumentException("Unknown ViewModel class")
   }
}

次の点に注意してください:

  • 提供されている SleepTrackerViewModelFactory は、ViewModel と同じ引数を取り、ViewModelProvider.Factory を拡張します。
  • factory 内では、コードは create() をオーバーライドします。これは、引数として任意のクラスタイプを取り、viewModelを返します。
  • create() の本体で、コードは使用可能な SleepTrackerViewModel クラスがあることを確認し、ある場合はそのインスタンスを返します。それ以外の場合、コードは例外をスローします。

Tip: これは主に定型コードであるため、将来の view-model factory でコードを再利用できます。

SleepTrackerFragment を更新する

  1. SleepTrackerFragment で、アプリケーションコンテキストへの参照を取得します。binding の下の onCreateView() に参照を配置します。view-model factory provider に渡すには、この fragment がアタッチされているアプリへの参照が必要です。

valuenull の場合、requireNotNull 関数は IllegalArgumentException をスローします。

val application = requireNotNull(this.activity).application
  1. DAO への参照を介したデータソースへの参照が必要です。onCreateView() で、return の前に dataSource を定義します。database の DAO への参照を取得するには、SleepDatabase.getInstance(application).sleepDatabaseDao を使用します。
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. onCreateView() で、return の前に、viewModelFactoryインスタンスを作成します。dataSourceapplication を渡す必要があります。
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
  1. factory ができたので、SleepTrackerViewModel への参照を取得します。SleepTrackerViewModel::class.java パラメーターは、このオブジェクトのラインタイム Java class を参照します。
val sleepTrackerViewModel =
       ViewModelProvider(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)
  1. 完成したコードは次のようになります:
// Create an instance of the ViewModel Factory.
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

// Get a reference to the ViewModel associated with this fragment.
val sleepTrackerViewModel =
       ViewModelProvider(
               this, viewModelFactory).get(SleepTrackerViewModel::class.java)

onCreateView() 内は次のようになります:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {

        // Get a reference to the binding object and inflate the fragment views.
        val binding: FragmentSleepTrackerBinding = DataBindingUtil.inflate(
                inflater, R.layout.fragment_sleep_tracker, container, false)

        val application = requireNotNull(this.activity).application

        val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao

        val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)

        val sleepTrackerViewModel =
                ViewModelProvider(
                        this, viewModelFactory).get(SleepTrackerViewModel::class.java)

        return binding.root
    }

view model の data binding を追加する

基本的な ViewModel を配置したら、SleepTrackerFragment で data binding の設定を完了して、ViewModel を UI に接続する必要があります。

fragment_sleep_tracker.xml レイアウトファイル: 1. <data> ブロック内に、SleepTrackerViewModel クラスを参照する <variable> を作成します。

<data>
   <variable
       name="sleepTrackerViewModel"
       type="com.example.android.trackmysleepquality.sleeptracker.SleepTrackerViewModel" />
</data>

SleepTrackerFragment で:

  1. 現在の activity を binding のライフサイクル owner として設定します。次のコードを onCreateView() メソッド内の return ステートメントの前に追加します。
binding.setLifecycleOwner(this)
  1. sleepTrackerViewModel binding 変数を sleepTrackerViewModel に割り当てます。このコードを onCreateView()内、SleepTrackerViewModel を作成するコードのしたに配置します。
binding.sleepTrackerViewModel = sleepTrackerViewModel
  1. プロジェクトをクリーンアップして再構築し、エラーを取り除きます。

  2. 最後に、いつのもように、コードがエラーなしでビルドおよび実行されることを確認してください。

Coroutines

メインスレッドをブロックせずにタスクを実行するための1つのパターンは、callbacks) を使用することです。マルチスレッドとコールバックの概要については Multi-threading & callbacks primer を参照してください。

Kotlin では、coroutine は長時間実行されるタスクをエレガントかつ効率的に処理する方法です。Kotlin coroutine を使用すると、コールバックベースのシーケンシャルコードに変換できます。通常、順番に記述されたコードは読みやすく、例外などの言語機能を使用することもできます。結局、coroutine とコールバックは同じことを行います。つまり、実行時間の長いタスクから結果が得られるまで待機し、実行を続行します。

Coroutines には次のプロパティがあります:

  • Coroutine は非同期で非ブロッキングです
  • Coroutine は suspended 関数を使用して非同期コードをシーケンシャルにします

Coroutine は非同期

coroutine は、プログラムの主要な実行ステップとは独立して実行されます。これは、並列または別のプロセッサ上にある可能性があります。また、アプリの残りの部分が入力を待っている間に、少しの処理をこっそり行っている可能性もあります。async(非同期) の重要な側面の1つは、明示的に待つまで、結果が利用可能であると期待できないことです。

例えば、調査が必要な質問があり、同僚に答えを見つけるように依頼したとします。彼らは立ち去ってそれに取り組んでいます。これは非同期に別のスレッドで作業を行っているようなものです。同僚が戻ってきて答えがなんであるかを教えてくれるまで、答えに依存しない他の作業を続けることができます。

Coroutine は no-blocking

None-blocking とは、coroutine がメインスレッドをまたは UI thread をブロックしないことを意味します。したがって、coroutine を使用すると、UI interaction が常に優先されるため、ユーザは常に可能な限りスムーズなエクスペリエンスを得ることができます。

Coroutine は suspended 関数を使用して非同期コードを sequential にする

キーワード suspend は、coroutine で使用できるものとして関数または関数タイプをマークする Kotlin の方法です。coroutine が suspend マークが付いた関数を呼び出すと、通常の関数呼び出しのように関数が戻るまでブロックするのではなく、結果の準備ができるまで coroutine は実行を一時停止します。その後、coroutine は中断したところから再開し、結果が得られます。

coroutine が中断されて結果を待っている間、coroutineは実行中のスレッドのブロックを解除します。そうすれば、他の関数や coroutine を実行できます。

suspend キーワードは、コードが実行されるスレッドを指定しません。suspended 関数はバックグランドスレッドまたはメインスレッドで実行できます。

Tip: ブロックと一時停止の違いは、スレッドがブロックされた場合、他の作業は発生しないことです。スレッドが中断された場合、結果が利用可能になるまで他の作業が行われます。

Kotlin で coroutine を使用するには、次の3つが必要です:

  • job
  • disptacher
  • scope

Job: 基本的に job はキャンセルできるものです。全ての coroutine には job があり、その job を使用して coroutine をキャンセルできます。job は親子階層に配置できます。親 job をキャンセルすると、job の全ての子がすぐにキャンセルされます。これは、各 coroutine を手動でキャンセルするよりもはるかに便利です。

Dispatcher: Dispatcher は、様々なスレッドで実行するために coroutine を送信します。たとえば、Dispatcher.Main はメインスレッドでタスクを実行し、Dispatcher.IOブロッキング I/O タスクをスレッドの共有プールにオフロードします。

Scope: Coroutine のスコープは、Coroutine が実行されるコンテキストを定義します。Scope は、Coroutine の job と dispatcher に関する情報を組み合わせたものです。scope は coroutine を追跡します。coroutine を起動すると、"in a scope" になります。つまり、どのスコープが coroutine を追跡するかを指定したことになります。

Architecture components を備えた Coroutine

CouroutineScope: CoroutineScope は、全ての coroutine を追跡し、coroutine をいつ実行するかを管理するのに役立ちます。また、開始された全ての Coroutine をキャンセルすることもできます。各非同期操作または Coroutine は、特定のスコープ内で実行されます。

Architecture componets は、アプリの論理スコープの Coroutine に対するファーストクラスのサポートを提供します。組み込みの Coroutine scopes は、対応する各アーキテクチャコンポーネントKTX extensions にあります。これらのスコープを使用する時は、必ず適切な依存関係を追加してください。

ViewModelScope: ViewModelScope は、アプリ内の ViewModel ごとに定義されます。このスコープで起動された Coroutine は、ViewModel がクリアされると同時にキャンセルされます。このコードラボでは、ViewModelScope を使用してデータベース操作を開始します。

Room and Dispatcher

Room ライブラリを使用してデータベース操作を実行する場合、Room は Dispatcher.IO を使用して、データベース操作をバックグランドで実行します。Dispatcher を明示的に指定する必要はありません。

データを表示して表示する

ユーザが次の方法で睡眠データを操作できるようにする必要があります: - ユーザが Start ボタンをタップすると、アプリは新しい sleep night を作成し、データベースに sleep night を保存します。 - ユーザが Stop ボタンをタップすると、アプリは night を終了時刻で更新します。 - ユーザが Clear ボタンをタップすると、アプリはデータベース内のデータを削除します。

これらのデータベース操作には時間がかかる可能性があるため、別のスレッドで実行する必要があります。

DAO 関数をサスペンド関数としてマークする

SleepDatabaseDao.kt で、convenience method を suspend functions に更新します。

  1. database/SleepDatabaseDao.kt を開き、getAllNights() を除く全てのメソッドに suspend キーワードを追加します。
@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>>
}

データベース操作用の Coroutine を設定する

Sleep Tracker アプリの Start ボタンをタップしたら、SleepTrackerViewModel の関数を呼び出して、SleepNight の新しいインスタンスを作成し、そのインスタンスをデータベースに保存します。

いずれかのボタンをタップすると、SleepNight の作成や更新などのデータベース操作がトリガーされます。この理由やその他の理由から、Coroutine を使用してアプリのボタンのクリックハンドラーを実装します。

  1. アプリレベルの build.gradle ファイルを開きます。依存関係セクションの下に、追加されたこれらの依存関係が必要です。
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() を呼び出します。次のステップで initializeTonight() を定義します。
init {
   initializeTonight()
}
  1. init ブロックの下に、initializeTonight() を実装します。viewModelScope.launch を使用して、ViewModelScope で coroutine を開始します。中括弧内で、getTonightFromDatabase() を呼び出してデータベースから今夜の値を取得し、その値を tonight.value に割り当てます。次のステップで getTonightFromDatabase() を定義します。
private fun initializeTonight() {
   viewModelScope.launch {
       tonight.value = getTonightFromDatabase()
   }
}
  1. getTonightFromDatabase() を実装します。現在開始されている SleepNight がない場合、nullable の SleepNight を返す private suspend 関数として定義します。関数に戻り値がないため、Error が発生します。
private suspend fun getTonightFromDatabase(): SleepNight? { }
  1. getTonightFromDatabase() の関数本体内で、database から tonight(最新の夜) を取得します。開始時刻と終了時刻が同じでない場合、つまり夜がすでに完了している場合は、null を返します。それ以外の場合は、night を返します。
       var night = database.getTonight()
       if (night?.endTimeMilli != night?.startTimeMilli) {
           night = null
       }
       return night

完成した getTonightFromDatabase() suspend 関数は次のようになります。

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

Start button のクリックハンドラーを追加

これで、Start ボタンのクリックハンドラーである onStartTracking() を実装できます。新しい SleepNight を作成してデータベースに挿入し、tonight に割り当てる必要があります。onStartTracking() の構造は、initializeTonight() と非常によく似ています。

  1. onStartTracking() の関数定義から始めます。SleepTrackerViewModel ファイルの onCleared() の上にあるクリックハンドラーを配置できます。
fun onStartTracking() {}
  1. onStartTracking() 内で、viewModelScope で coroutine を起動します。これは、UI を続行して更新するために、この結果が必要だからです。
viewModelScope.launch {}
  1. Coroutine の起動内で、現在の時刻を開始時刻としてキャプチャする新しい SleepNight を作成します。
        val newNight = SleepNight()
  1. Coroutine 起動内で、insert() を呼び出して newNight をデータベースに挿入します。この insert() サスペンド関数をまだ定義していないため、エラーが表示されます(同名の DAO 機能ではありません)。
       insert(newNight)
  1. また、Coroutine の起動内で、tonight を更新します。
       tonight.value = getTonightFromDatabase()
  1. onStartTracking() の下で、insert()SleepNight を引数としてとる private suspend 関数として定義します。
private suspend fun insert(night: SleepNight) {}
  1. insert() メソッド内で、DAO を使用して database を tonight を挿入します。
       database.insert(night)

Room の Coroutine は Dispatchers.IO を使用するため、これはメインスレッドで発生しないことに注意してください。

  1. fragment_sleep_tracker.xml レイアウトファイルで、前に設定した binding の magic を使用してonStartTracking() のクリックハンドラーを start_button に追加します。@{() -&gt; 関数表記は、引数を取らず、sleepTrackerViewModel のクリックハンドラーを呼び出すラムダ関数を作成します。
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
  1. アプリをビルドして実行します。Start ボタンをタップします。このアクションはデータを作成しますが、まだ何も表示されません。次にこれを修正します。

Important: これでパターンが表示されます。 1. 結果が UI に影響するため、メインスレッドまたは UI スレッドで実行される Coroutine を起動します。次の例に示すように、ViewModel の viewModelScope プロパティを介して ViewModel の CoroutineScope にアクセスできます。 2. suspend function を呼び出して長時間実行される作業を実行し、結果を待っている間に UI スレッドをブロックしないようにします。 3. 長時間実行される作業は、UI とは何の関係もありません。I/O dispatcher に切り替えて、これらの種類の操作用に最適化されて確保されているスレッドプールで作業を実行できるようにします。 4. 次に、長時間実行されている関数を呼び出して作業を行います。

パターンを以下に示します。

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

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

Using Room

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

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

データを表示する

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

これは、データベース内のデータが変更されるたびに、LiveDatanight が更新されて最新のデータが表示される Room の機能です。LiveData を明示的に設定したり更新したりする必要はありません。Room は、データベースと一致するようにデータを更新します。

ただし、TextView で night を表示すると、オブジェクトが参照されます。オブジェクトの内容を表示するには、データをフォーマットされた文字列に変換します。night データベースから新しいデータを受信するたびに実行される Transformation map を使用します。

  1. Util.kt ファイルを開き、formatNights() および関連する import ステート麺との定義のコードのコメントを解除します。
  2. formatNights() は、HTML形式の文字列である Spannded 型を返すことに注意してください。
  3. strings.xml を開きます。睡眠データを表示するための文字列リソースをフォーマットするために CDATA を使用していることに注意してください。
  4. SleepTrackerViewModel を開きます。SleepTrackerViewModel クラスで、nights という変数を定義します。データベースから全ての nights を取得し、それらを nights 変数に割り当てます。
private val nights = database.getAllNights()
  1. nights の定義のすぐ下に、nightsnightsString に変換するコードを追加します。Util.ktformatNights() 関数を使用します。

Transformations クラスから map() 関数に nights を渡します。文字列リソースにアクセスするには、formatNights() を呼び出すように mapping 関数を定義します。nightsResources オブジェクトを提供します。

val nightsString = Transformations.map(nights) { nights ->
   formatNights(nights, application.resources)
}
  1. fragment_sleep_tracker.xml レイアウトファイルを開きます。TextViewandroid:text プロパティで、リソース文字列を nightsString への参照に置き換えることができるようになりました。
"@{sleepTrackerViewModel.nightsString}"
  1. コードを再構築してアプリを実行します。開始時刻を含む全ての睡眠データが表示されます。

  1. Start ボタンをさらに数回タップすると、より多くのデータが表示されます。

次のステップでは、Stop ボタンの機能を有効にします。

Stop ボタンのクリックハンドラーを追加します

前の手順と同じパターンを使用して、SleepTrackerViewModelStop ボタンのクリックハンドラーを実装します。

  1. ViewModelonStopTracking() を追加します。viewModelScopeで Coroutine を起動します。終了時刻がまだ設定されていない場合は、endTimeMilli を現在のシステム時刻に設定し、night data を使用して update() を呼び出します。

Kotlin では、return@label 構文は、いくつかのネストされた関数の中で、このステートメントが返される関数を指定します。

fun onStopTracking() {
   viewModelScope.launch {
       val oldNight = tonight.value ?: return@launch
       oldNight.endTimeMilli = System.currentTimeMillis()
       update(oldNight)
   }
}
  1. insert() の実装に使用したのと同じパターンを使用して update() を実装します。
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 をタップします。開始時刻、終了時刻、値のない睡眠の質、および睡眠時間が表示されます。

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 をタップして、全てのデータを削除します。次に StartStop をタップして新しいデータを作成します。

まとめ

  • ViewModelViewModelFactory および data binding を使用して、アプリの UI アーキテクチャを設定します。
  • UI をスムーズに実行し続けるには、全ての database 操作など、実行時間の長いタスクに Coroutine を使用します。
  • Coroutine は非同期で非ブロッキングです。それらは suspend 関数を使用して非同期コードをシーケンシャルにします。
  • Coroutine が suspendのマークがついた関数を呼び出すと、その関数が通常の関数呼び出しのように戻るまでブロックするのではなく、結果の準備ができるまで実行を suspend します。次に、中断をしたところから再開して結果を出します。
  • ブロックとサスペンドの違いは、スレッドがブロックされた場合、他の作業は発生しないことです。スレッドがサスペンドされた場合、結果が利用可能になるまで他の作業が行われます。

database 操作をトリガーするクリックハンドラーを実装するには、次のパターンに従います。

  1. 結果が UI に影響するため、メインスレッドまたは UI スレッドで実行される Coroutine を起動します。
  2. suspend functions を呼び出して長時間実行される作業を実行し、結果を待っている間に UI スレッドをブリックしないようにします。
  3. 長時間実行される作業は UI とは関係がないため、I/O コンテキストに切り替えます。そうすれば、これらの種類の操作のために最適化されて取っておかれるスレッドプールで作業を実行できます。
  4. 次に、長時間実行されている関数を呼び出して作業を行います。

Transformation map を使用して、オブジェクトが変更されるたびに LiveData オブジェクトから文字列を作成します。

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com