学ぶこと
- Android でスレッドがどのように機能するか
- Kotlin coroutines を使用して、database 操作をメインスレッドから移動する方法
- フォーマットされたデータ
TextView
に表示する方法
すること
- TrackMySleepQuality アプリを拡張して、データベースとの間でデータを収集、保存、および表示します。
- coroutines を使用して、データベース操作をバックグランドで実行します。
LiveData
を使用して、navigation と snackbar の表示をトリガーします。LiveData
を使用して、ボタンを有効または無効にします。
Starter code を調べる
このタスクでは、TextView
を使用して、フォーマットされた睡眠追跡データを表示します。
res/layout/activity_main.xml
を開きます。このレイアウトには、nav_host_fragment
fragment が含まれています。また、<merge>
タグにも注目してください。
merge
タグは、レイアウトを含める時に冗長なレイアウトを排除するために使用できます。これを使用することが推奨されています。冗長レイアウトの例は、ConstraintLayout > LinearLayout > TextView であり、システムは LinearLayout を排除できる可能性があります。この種の最適化により、View 階層が簡素化され、アプリのパフォーマンスが向上します。
navigation フォルダーで、navigation.xml を開きます。2つの Fragmnet と、それらを接続する navigation action を確認できます。
layout フォルダーで、sleep tracker fragment をダブルクリックして、XML layout を表示します。次の点に集中してください。
layout data は、data binding を有効にするために
<layout>
要素でラップされます。ConstraintLayout
およびその他の View は、<layout>
要素内に配置されます。- ファイルにはプレースホルダー
<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 を追加します
- sleeptracker パッケージで、SleepTrackerViewModel.kt を開きます。
- スターターアプリで提供され、以下にも示されている
SleepTrackerViewModel
クラスを調べます。クラスはAndroidViewModel
を拡張することに注意してください。このクラスはViewModel
と同じですが、アプリケーションコンテキストをコンストラクターパラメーターとして受け取り、プロパティとして使用できるようにします。これは後で必要になります。
class SleepTrackerViewModel( val database: SleepDatabaseDao, application: Application) : AndroidViewModel(application) { }
SleepTrackerViewModelFactory を追加します
sleeptracker パッケージで、SleepTrackerViewModelFactory.kt を開きます。
以下に示す、ファクトリ用に提供されているいるコードを調べます。
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 を更新する
SleepTrackerFragment
で、アプリケーションコンテキストへの参照を取得します。binding
の下のonCreateView()
に参照を配置します。view-model factory provider に渡すには、この fragment がアタッチされているアプリへの参照が必要です。
value が null
の場合、requireNotNull
関数は IllegalArgumentException
をスローします。
val application = requireNotNull(this.activity).application
- DAO への参照を介したデータソースへの参照が必要です。
onCreateView()
で、return
の前にdataSource
を定義します。database の DAO への参照を取得するには、SleepDatabase.getInstance(application).sleepDatabaseDao
を使用します。
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
onCreateView()
で、return
の前に、viewModelFactory
のインスタンスを作成します。dataSource
とapplication
を渡す必要があります。
val viewModelFactory = SleepTrackerViewModelFactory(dataSource, application)
- factory ができたので、
SleepTrackerViewModel
への参照を取得します。SleepTrackerViewModel::class.java
パラメーターは、このオブジェクトのラインタイム Java class を参照します。
val sleepTrackerViewModel = ViewModelProvider( this, viewModelFactory).get(SleepTrackerViewModel::class.java)
- 完成したコードは次のようになります:
// 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
で:
- 現在の activity を binding のライフサイクル owner として設定します。次のコードを
onCreateView()
メソッド内のreturn
ステートメントの前に追加します。
binding.setLifecycleOwner(this)
sleepTrackerViewModel
binding 変数をsleepTrackerViewModel
に割り当てます。このコードをonCreateView()
内、SleepTrackerViewModel
を作成するコードのしたに配置します。
binding.sleepTrackerViewModel = sleepTrackerViewModel
プロジェクトをクリーンアップして再構築し、エラーを取り除きます。
最後に、いつのもように、コードがエラーなしでビルドおよび実行されることを確認してください。
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 に更新します。
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 を使用してアプリのボタンのクリックハンドラーを実装します。
- アプリレベルの
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"
SleepTrackerViewModel
ファイルを開きます。現在の夜を保持するために
tonight
という変数を定義します。データを関しして変更できる必要があるため、MutableLiveData
を作成します。
private var tonight = MutableLiveData<SleepNight?>()
tonight
変数をできるだけ早く初期化するには、tonight
の定義の下にinit
ブロックを作成し、initializeTonight()
を呼び出します。次のステップでinitializeTonight()
を定義します。
init {
initializeTonight()
}
init
ブロックの下に、initializeTonight()
を実装します。viewModelScope.launch
を使用して、ViewModelScope
で coroutine を開始します。中括弧内で、getTonightFromDatabase()
を呼び出してデータベースから今夜の値を取得し、その値をtonight.value
に割り当てます。次のステップでgetTonightFromDatabase()
を定義します。
private fun initializeTonight() { viewModelScope.launch { tonight.value = getTonightFromDatabase() } }
getTonightFromDatabase()
を実装します。現在開始されているSleepNight
がない場合、nullable のSleepNight
を返すprivate suspend
関数として定義します。関数に戻り値がないため、Error が発生します。
private suspend fun getTonightFromDatabase(): SleepNight? { }
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()
と非常によく似ています。
onStartTracking()
の関数定義から始めます。SleepTrackerViewModel
ファイルのonCleared()
の上にあるクリックハンドラーを配置できます。
fun onStartTracking() {}
onStartTracking()
内で、viewModelScope
で coroutine を起動します。これは、UI を続行して更新するために、この結果が必要だからです。
viewModelScope.launch {}
- Coroutine の起動内で、現在の時刻を開始時刻としてキャプチャする新しい
SleepNight
を作成します。
val newNight = SleepNight()
- Coroutine 起動内で、
insert()
を呼び出してnewNight
をデータベースに挿入します。このinsert()
サスペンド関数をまだ定義していないため、エラーが表示されます(同名の DAO 機能ではありません)。
insert(newNight)
- また、Coroutine の起動内で、
tonight
を更新します。
tonight.value = getTonightFromDatabase()
onStartTracking()
の下で、insert()
をSleepNight
を引数としてとるprivate suspend
関数として定義します。
private suspend fun insert(night: SleepNight) {}
insert()
メソッド内で、DAO を使用して database を tonight を挿入します。
database.insert(night)
Room の Coroutine は Dispatchers.IO
を使用するため、これはメインスレッドで発生しないことに注意してください。
fragment_sleep_tracker.xml
レイアウトファイルで、前に設定した binding の magic を使用してonStartTracking()
のクリックハンドラーをstart_button
に追加します。@{() ->
関数表記は、引数を取らず、sleepTrackerViewModel
のクリックハンドラーを呼び出すラムダ関数を作成します。
android:onClick="@{() -> sleepTrackerViewModel.onStartTracking()}"
- アプリをビルドして実行します。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
を参照します。
これは、データベース内のデータが変更されるたびに、LiveData
の night
が更新されて最新のデータが表示される Room
の機能です。LiveData
を明示的に設定したり更新したりする必要はありません。Room
は、データベースと一致するようにデータを更新します。
ただし、TextView で night
を表示すると、オブジェクトが参照されます。オブジェクトの内容を表示するには、データをフォーマットされた文字列に変換します。night
データベースから新しいデータを受信するたびに実行される Transformation
map を使用します。
Util.kt
ファイルを開き、formatNights()
および関連するimport
ステート麺との定義のコードのコメントを解除します。formatNights()
は、HTML形式の文字列であるSpannded
型を返すことに注意してください。strings.xml
を開きます。睡眠データを表示するための文字列リソースをフォーマットするためにCDATA
を使用していることに注意してください。- SleepTrackerViewModel を開きます。
SleepTrackerViewModel
クラスで、nights
という変数を定義します。データベースから全てのnights
を取得し、それらをnights
変数に割り当てます。
private val nights = database.getAllNights()
nights
の定義のすぐ下に、nights
をnightsString
に変換するコードを追加します。Util.kt
のformatNights()
関数を使用します。
Transformations
クラスから map()
関数に nights
を渡します。文字列リソースにアクセスするには、formatNights()
を呼び出すように mapping 関数を定義します。nights
と Resources
オブジェクトを提供します。
val nightsString = Transformations.map(nights) { nights -> formatNights(nights, application.resources) }
fragment_sleep_tracker.xml
レイアウトファイルを開きます。TextView
のandroid:text
プロパティで、リソース文字列をnightsString
への参照に置き換えることができるようになりました。
"@{sleepTrackerViewModel.nightsString}"
- コードを再構築してアプリを実行します。開始時刻を含む全ての睡眠データが表示されます。
- Start ボタンをさらに数回タップすると、より多くのデータが表示されます。
次のステップでは、Stop ボタンの機能を有効にします。
Stop ボタンのクリックハンドラーを追加します
前の手順と同じパターンを使用して、SleepTrackerViewModel
の Stop ボタンのクリックハンドラーを実装します。
ViewModel
にonStopTracking()
を追加します。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) } }
insert()
の実装に使用したのと同じパターンを使用してupdate()
を実装します。
private suspend fun update(night: SleepNight) { database.update(night) }
- クリックハンドラーを UI に接続するには、
fragment_sleep_tracker.xml
レイアウトファイルを開き、クリックハンドラーをstop_button
に追加します。
android:onClick="@{() -> sleepTrackerViewModel.onStopTracking()}"
アプリをビルドして実行します。
Start をタップしてから、Stop をタップします。開始時刻、終了時刻、値のない睡眠の質、および睡眠時間が表示されます。
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 をタップして新しいデータを作成します。
まとめ
ViewModel
、ViewModelFactory
および data binding を使用して、アプリの UI アーキテクチャを設定します。- UI をスムーズに実行し続けるには、全ての database 操作など、実行時間の長いタスクに Coroutine を使用します。
- Coroutine は非同期で非ブロッキングです。それらは
suspend
関数を使用して非同期コードをシーケンシャルにします。 - Coroutine が
suspend
のマークがついた関数を呼び出すと、その関数が通常の関数呼び出しのように戻るまでブロックするのではなく、結果の準備ができるまで実行をsuspend
します。次に、中断をしたところから再開して結果を出します。 - ブロックとサスペンドの違いは、スレッドがブロックされた場合、他の作業は発生しないことです。スレッドがサスペンドされた場合、結果が利用可能になるまで他の作業が行われます。
database 操作をトリガーするクリックハンドラーを実装するには、次のパターンに従います。
- 結果が UI に影響するため、メインスレッドまたは UI スレッドで実行される Coroutine を起動します。
- suspend functions を呼び出して長時間実行される作業を実行し、結果を待っている間に UI スレッドをブリックしないようにします。
- 長時間実行される作業は UI とは関係がないため、I/O コンテキストに切り替えます。そうすれば、これらの種類の操作のために最適化されて取っておかれるスレッドプールで作業を実行できます。
- 次に、長時間実行されている関数を呼び出して作業を行います。
Transformation
map を使用して、オブジェクトが変更されるたびに LiveData
オブジェクトから文字列を作成します。