この Codelab は、Android Kotlin の基礎コースの一部です。このコースを最大限に活用するには、Codelab を順番に進めることをおすすめします。コースのすべての Codelab は、Android Kotlin の基礎の Codelab のランディング ページに一覧表示されています。
はじめに
この Codelab では、ViewModel
とフラグメントを組み合わせてナビゲーションを実装する方法を復習します。目標は、ナビゲーションの when のロジックを ViewModel
に配置し、パスをフラグメントとナビゲーション ファイルで定義することです。この目標を達成するには、ビューモデル、フラグメント、LiveData
、オブザーバーを使用します。
この Codelab の最後では、ボタンの状態を最小限のコードで追跡する賢い方法を紹介します。これにより、各ボタンは、ユーザーがそのボタンをタップする意味がある場合にのみ有効になり、クリックできるようになります。
前提となる知識
以下について把握しておく必要があります。
- アクティビティ、フラグメント、ビューを使用して基本的なユーザー インターフェース(UI)を構築する。
- フラグメント間の移動と、
safeArgs
を使用してフラグメント間でデータを渡す。 - モデル、モデル ファクトリ、変換、
LiveData
、オブザーバーを表示します。 Room
データベースを作成し、データ アクセス オブジェクト(DAO)を作成して、エンティティを定義する方法。- データベース インタラクションやその他の長時間実行タスクにコルーチンを使用する方法。
学習内容
- データベース内の既存の睡眠の質の記録を更新する方法。
LiveData
を使用してボタンの状態を追跡する方法。- イベントに応答してスナックバーを表示する方法。
演習内容
- TrackMySleepQuality アプリを拡張して品質評価を収集し、その評価をデータベースに追加して結果を表示します。
LiveData
を使用してスナックバーの表示をトリガーします。LiveData
を使用してボタンを有効または無効にします。
この Codelab では、TrackMySleepQuality アプリの睡眠の質の記録と最終的な UI を作成します。
このアプリには、下の図に示すように、フラグメントで表される 2 つの画面があります。
左側に表示されている最初の画面には、トラッキングの開始と停止を行うボタンがあります。画面には、ユーザーのすべての睡眠データが表示されます。[消去] ボタンをクリックすると、アプリがユーザーのために収集したすべてのデータが完全に削除されます。
右側の 2 つ目の画面は、睡眠の質の評価を選択するためのものです。アプリでは、評価は数値で表されます。開発目的で、アプリには顔アイコンとその数値が両方表示されます。
ユーザーのフローは次のとおりです。
- ユーザーがアプリを開くと、睡眠トラッキング画面が表示されます。
- ユーザーが [開始] ボタンをタップします。開始時間が記録され、表示されます。[開始] ボタンが無効になり、[停止] ボタンが有効になります。
- ユーザーが [停止] ボタンをタップします。終了時刻が記録され、睡眠の質の画面が開きます。
- ユーザーが睡眠の質のアイコンを選択します。画面が閉じ、トラッキング画面に睡眠終了時刻と睡眠の質が表示されます。[停止] ボタンは無効になり、[開始] ボタンは有効になります。アプリは次の夜の準備ができています。
- [クリア] ボタンは、データベースにデータがある場合は常に有効になります。ユーザーが [クリア] ボタンをタップすると、確認メッセージが表示されることなく、すべてのデータが消去されます。
このアプリは、次の図に示すように、簡略化されたアーキテクチャを使用します。このアプリでは、次のコンポーネントのみを使用します。
- UI コントローラ
- モデルと
LiveData
を表示する - Room データベース
この Codelab は、フラグメントとナビゲーション ファイルを使用してナビゲーションを実装する方法を理解していることを前提としています。作業を保存するために、このコードの多くが提供されています。
ステップ 1: コードを検査する
- まず、前回の Codelab の最後に作成したコードを使用するか、スターター コードをダウンロードします。
- スターター コードで、
SleepQualityFragment
を調べます。このクラスは、レイアウトを拡張し、アプリケーションを取得して、binding.root
を返します。 - デザイン エディタで navigation.xml を開きます。
SleepTrackerFragment
からSleepQualityFragment
へのナビゲーション パスがあり、SleepQualityFragment
からSleepTrackerFragment
に戻るパスがあることがわかります。 - navigation.xml のコードを検査します。特に、
sleepNightKey
という名前の<argument>
を探します。
ユーザーがSleepTrackerFragment
からSleepQualityFragment,
に移動すると、アプリは更新が必要な夜間のsleepNightKey
をSleepQualityFragment
に渡します。
ステップ 2: 睡眠の質のトラッキングのナビゲーションを追加する
ナビゲーション グラフには、SleepTrackerFragment
から SleepQualityFragment
へのパスと、そこから戻るパスがすでに含まれています。ただし、あるフラグメントから次のフラグメントへのナビゲーションを実装するクリック ハンドラはまだコード化されていません。このコードを ViewModel
に追加します。
クリック ハンドラで、アプリが別のデスティネーションに移動するタイミングで変更される LiveData
を設定します。フラグメントはこの LiveData
を監視します。データが変更されると、フラグメントは宛先に移動し、ビューモデルに完了したことを伝えます。これにより、状態変数がリセットされます。
SleepTrackerViewModel
を開きます。ユーザーが [Stop] ボタンをタップしたときに、アプリがSleepQualityFragment
に移動して品質評価を収集するように、ナビゲーションを追加する必要があります。SleepTrackerViewModel
で、アプリがSleepQualityFragment
に移動するタイミングで変更されるLiveData
を作成します。カプセル化を使用して、LiveData
の取得可能なバージョンのみをViewModel
に公開します。
このコードは、クラス本体の最上位の任意の場所に配置できます。
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()
val navigateToSleepQuality: LiveData<SleepNight>
get() = _navigateToSleepQuality
- ナビゲーションをトリガーする変数をリセットする
doneNavigating()
関数を追加します。
fun doneNavigating() {
_navigateToSleepQuality.value = null
}
- [停止] ボタンのクリック ハンドラ
onStopTracking()
で、SleepQualityFragment
へのナビゲーションをトリガーします。関数の末尾で、launch{}
ブロック内の最後の要素として _navigateToSleepQuality
変数を設定します。この変数はnight
に設定されています。この変数に値がある場合、アプリはSleepQualityFragment
に移動し、night.
を渡します。
_navigateToSleepQuality.value = oldNight
SleepTrackerFragment
は、アプリがナビゲーションを行うタイミングを把握できるように、_navigateToSleepQuality
を監視する必要があります。SleepTrackerFragment
のonCreateView()
で、navigateToSleepQuality()
のオブザーバーを追加します。このインポートは曖昧であるため、androidx.lifecycle.Observer
をインポートする必要があります。
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
- オブザーバー ブロック内で、ナビゲートして現在の夜の ID を渡し、
doneNavigating()
を呼び出します。インポートがあいまいな場合は、androidx.navigation.fragment.findNavController
をインポートします。
night ->
night?.let {
this.findNavController().navigate(
SleepTrackerFragmentDirections
.actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
sleepTrackerViewModel.doneNavigating()
}
- アプリをビルドして実行します。[Start] をタップしてから [Stop] をタップすると、
SleepQualityFragment
画面が表示されます。戻るには、システムの [戻る] ボタンを使用します。
このタスクでは、睡眠の質を記録し、睡眠トラッカー フラグメントに戻ります。表示は自動的に更新され、更新された値がユーザーに表示されます。ViewModel
と ViewModelFactory
を作成し、SleepQualityFragment
を更新する必要があります。
ステップ 1: ViewModel と ViewModelFactory を作成する
sleepquality
パッケージで、SleepQualityViewModel.kt を作成または開きます。sleepNightKey
とデータベースを引数として受け取るSleepQualityViewModel
クラスを作成します。SleepTrackerViewModel
の場合と同様に、ファクトリからdatabase
を渡す必要があります。ナビゲーションからsleepNightKey
も渡す必要があります。
class SleepQualityViewModel(
private val sleepNightKey: Long = 0L,
val database: SleepDatabaseDao) : ViewModel() {
}
SleepQualityViewModel
クラス内で、Job
とuiScope
を定義し、onCleared()
をオーバーライドします。
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
- 上記と同じパターンを使用して
SleepTrackerFragment
に戻るには、_navigateToSleepTracker
を宣言します。navigateToSleepTracker
とdoneNavigating()
を実装します。
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()
val navigateToSleepTracker: LiveData<Boolean?>
get() = _navigateToSleepTracker
fun doneNavigating() {
_navigateToSleepTracker.value = null
}
- 使用するすべての睡眠の質の画像に対して、クリック ハンドラ
onSetSleepQuality()
を 1 つ作成します。
前の Codelab と同じコルーチン パターンを使用します。
uiScope
でコルーチンを開始し、I/O ディスパッチャに切り替えます。sleepNightKey
を使用してtonight
を取得します。- 睡眠の質を設定します。
- データベースを更新します。
- ナビゲーションをトリガーします。
次のコードサンプルでは、データベース オペレーションを別のコンテキストにファクタリングするのではなく、クリック ハンドラですべての処理を行っています。
fun onSetSleepQuality(quality: Int) {
uiScope.launch {
// IO is a thread pool for running operations that access the disk, such as
// our Room database.
withContext(Dispatchers.IO) {
val tonight = database.get(sleepNightKey) ?: return@withContext
tonight.sleepQuality = quality
database.update(tonight)
}
// Setting this state variable to true will alert the observer and trigger navigation.
_navigateToSleepTracker.value = true
}
}
sleepquality
パッケージで、以下に示すようにSleepQualityViewModelFactory.kt
を作成または開き、SleepQualityViewModelFactory
クラスを追加します。このクラスでは、以前に見たものと同じボイラープレート コードのバージョンを使用します。次に進む前にコードを確認してください。
class SleepQualityViewModelFactory(
private val sleepNightKey: Long,
private val dataSource: SleepDatabaseDao) : ViewModelProvider.Factory {
@Suppress("unchecked_cast")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(SleepQualityViewModel::class.java)) {
return SleepQualityViewModel(sleepNightKey, dataSource) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
ステップ 2: SleepQualityFragment を更新する
SleepQualityFragment.kt
を開きます。onCreateView()
でapplication
を取得したら、ナビゲーションに付属するarguments
を取得する必要があります。これらの引数はSleepQualityFragmentArgs
にあります。バンドルから抽出する必要があります。
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
- 次に、
dataSource
を取得します。
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
dataSource
とsleepNightKey
を渡してファクトリを作成します。
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
ViewModel
参照を取得します。
val sleepQualityViewModel =
ViewModelProviders.of(
this, viewModelFactory).get(SleepQualityViewModel::class.java)
- バインディング オブジェクトに
ViewModel
を追加します。(バインディング オブジェクトでエラーが発生した場合は、現時点では無視してください)。
binding.sleepQualityViewModel = sleepQualityViewModel
- オブザーバーを追加します。プロンプトが表示されたら、
androidx.lifecycle.Observer
をインポートします。
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
if (it == true) { // Observed state is true.
this.findNavController().navigate(
SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
sleepQualityViewModel.doneNavigating()
}
})
ステップ 3: レイアウト ファイルを更新してアプリを実行する
fragment_sleep_quality.xml
レイアウト ファイルを開きます。<data>
ブロックにSleepQualityViewModel
の変数を追加します。
<data>
<variable
name="sleepQualityViewModel"
type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
</data>
- 6 つの睡眠の質を表す画像それぞれに、次のようなクリック ハンドラを追加します。画質評価を画像に一致させます。
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
- プロジェクトをクリーンして再ビルドします。これで、バインディング オブジェクトのエラーが解決されます。それ以外の場合は、キャッシュをクリア([File] > [Invalidate Caches / Restart])してアプリを再ビルドします。
これで、コルーチンを使用して、完全な Room
データベース アプリを作成しました。
これで、アプリは正常に動作します。ユーザーは [開始] と [停止] を何度でもタップできます。[停止] をタップすると、睡眠の質を入力できます。ユーザーが [クリア] をタップすると、すべてのデータがバックグラウンドで自動的にクリアされます。ただし、すべてのボタンが常に有効でクリック可能であるため、アプリが壊れることはありませんが、ユーザーが不完全な睡眠記録を作成できてしまいます。
この最後のタスクでは、変換マップを使用してボタンの表示 / 非表示を管理し、ユーザーが正しい選択肢のみを選択できるようにする方法について説明します。同様の方法で、すべてのデータが消去された後にフレンドリーなメッセージを表示できます。
ステップ 1: ボタンの状態を更新する
ボタンの状態を設定して、最初は [Start] ボタンのみが有効(クリック可能)になるようにします。
ユーザーが [開始] をタップすると、[停止] ボタンが有効になり、[開始] は無効になります。[Clear] ボタンは、データベースにデータがある場合にのみ有効になります。
fragment_sleep_tracker.xml
レイアウト ファイルを開きます。- 各ボタンに
android:enabled
プロパティを追加します。android:enabled
プロパティは、ボタンが有効かどうかを示すブール値です。(有効なボタンはタップできますが、無効なボタンはタップできません)。プロパティに、後で定義する状態変数の値を指定します。
start_button
:
android:enabled="@{sleepTrackerViewModel.startButtonVisible}"
stop_button
:
android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"
clear_button
:
android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
SleepTrackerViewModel
を開き、対応する 3 つの変数を作成します。各変数に、その変数をテストする変換を割り当てます。
tonight
がnull
の場合、[開始] ボタンは有効になっている必要があります。tonight
がnull
でない場合、[停止] ボタンは有効にする必要があります。- クリア ボタンは、
nights
、つまりデータベースに睡眠記録が含まれている場合にのみ有効にする必要があります。
val startButtonVisible = Transformations.map(tonight) {
it == null
}
val stopButtonVisible = Transformations.map(tonight) {
it != null
}
val clearButtonVisible = Transformations.map(nights) {
it?.isNotEmpty()
}
- アプリを実行し、ボタンを試してみます。
ステップ 2: スナックバーを使用してユーザーに通知する
ユーザーがデータベースをクリアしたら、Snackbar
ウィジェットを使用して確認を表示します。スナックバーは、画面下部のメッセージを通じて操作に関する簡単なフィードバックを提供します。スナックバーは、タイムアウト後、画面の別の場所でユーザーが操作を行った後、またはユーザーがスナックバーを画面外にスワイプした後に消えます。
スナックバーの表示は UI タスクであり、フラグメントで行う必要があります。スナックバーを表示するかどうかは ViewModel
で決定されます。データがクリアされたときにスナックバーを設定してトリガーするには、ナビゲーションをトリガーする場合と同じ手法を使用します。
SleepTrackerViewModel
で、カプセル化されたイベントを作成します。
private var _showSnackbarEvent = MutableLiveData<Boolean>()
val showSnackBarEvent: LiveData<Boolean>
get() = _showSnackbarEvent
- 次に、
doneShowingSnackbar()
を実装します。
fun doneShowingSnackbar() {
_showSnackbarEvent.value = false
}
SleepTrackerFragment
のonCreateView()
にオブザーバーを追加します。
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
- オブザーバー ブロック内で、スナックバーを表示し、イベントを直ちにリセットします。
if (it == true) { // Observed state is true.
Snackbar.make(
activity!!.findViewById(android.R.id.content),
getString(R.string.cleared_message),
Snackbar.LENGTH_SHORT // How long to display the message.
).show()
sleepTrackerViewModel.doneShowingSnackbar()
}
SleepTrackerViewModel
で、onClear()
メソッドのイベントをトリガーします。これを行うには、launch
ブロック内のイベント値をtrue
に設定します。
_showSnackbarEvent.value = true
- アプリをビルドして実行します。
Android Studio プロジェクト: TrackMySleepQualityFinal
このアプリに睡眠管理を実装することは、新しいキーでよく知られた曲を演奏するようなものです。詳細は異なりますが、このレッスンで以前の Codelab で行ったことの基本的なパターンは同じです。これらのパターンを認識しておくと、既存のアプリのコードを再利用できるため、コーディングが大幅に高速化されます。このコースでこれまで使用したパターンをいくつか紹介します。
ViewModel
とViewModelFactory
を作成し、データソースを設定します。- ナビゲーションをトリガーします。関心を分離するため、クリック ハンドラはビューモデルに、ナビゲーションはフラグメントに配置します。
LiveData
でカプセル化を使用して、状態の変化を追跡して対応します。LiveData
で変換を使用します。- シングルトン データベースを作成します。
- データベース オペレーション用にコルーチンを設定します。
ナビゲーションのトリガー
フラグメント間の可能なナビゲーション パスは、ナビゲーション ファイルで定義します。フラグメント間のナビゲーションをトリガーする方法はいくつかあります。たとえば、次のような疑問があります。
onClick
ハンドラを定義して、宛先フラグメントへのナビゲーションをトリガーします。- または、フラグメント間のナビゲーションを有効にするには:
- ナビゲーションを行うかどうかを記録する
LiveData
値を定義します。 - その
LiveData
値にオブザーバーをアタッチします。 - コードは、ナビゲーションをトリガーする必要があるときやナビゲーションが完了したときに、その値を変更します。
android:enabled 属性の設定
android:enabled
属性はTextView
で定義され、Button
を含むすべてのサブクラスによって継承されます。android:enabled
属性は、View
を有効にするかどうかを決定します。「有効」の意味はサブクラスによって異なります。たとえば、EditText
が有効でない場合、ユーザーは含まれているテキストを編集できません。また、Button
が有効でない場合、ユーザーはボタンをタップできません。enabled
属性はvisibility
属性と同じではありません。- 変換マップを使用すると、別のオブジェクトまたは変数の状態に基づいてボタンの
enabled
属性の値を設定できます。
この Codelab で説明するその他のポイントは次のとおりです。
- ユーザーに通知をトリガーするには、ナビゲーションをトリガーする場合と同じ手法を使用できます。
Snackbar
を使用してユーザーに通知できます。
Udacity コース:
Android デベロッパー ドキュメント:
このセクションでは、インストラクター主導のコースの一環として、この Codelab に取り組んでいる生徒向けに考えられる宿題をいくつか示します。インストラクターは、以下のようなことを行えます。
- 必要に応じて宿題を与える
- 宿題の提出方法を生徒に伝える
- 宿題を採点する
インストラクターは、これらの提案を必要なだけ使用し、必要に応じて他の宿題も自由に与えることができます。
この Codelab に独力で取り組む場合は、これらの宿題を自由に使用して知識をテストしてください。
以下の質問に回答してください
問題 1
アプリでフラグメント間のナビゲーションをトリガーできるようにする方法の 1 つは、LiveData
値を使用してナビゲーションをトリガーするかどうかを示すことです。
gotoBlueFragment
という LiveData
値を使用して、赤いフラグメントから青いフラグメントへのナビゲーションをトリガーする手順を教えてください。該当するものをすべてお選びください。
ViewModel
で、LiveData
値gotoBlueFragment
を定義します。RedFragment
でgotoBlueFragment
の値を確認します。observe{}
コードを実装して、必要に応じてBlueFragment
に移動し、gotoBlueFragment
の値をリセットして移動が完了したことを示します。- アプリが
RedFragment
からBlueFragment
に移動する必要があるたびに、コードがgotoBlueFragment
変数をナビゲーションをトリガーする値に設定するようにします。 - ユーザーがクリックして
BlueFragment
に移動するView
のonClick
ハンドラがコードで定義されていることを確認します。このonClick
ハンドラはgoToBlueFragment
値を監視します。
問題 2
LiveData
を使用して、Button
を有効(クリック可能)にするかどうかを変更できます。アプリで UpdateNumber
ボタンを次のように変更するには、どうすればよいですか?
myNumber
の値が 5 より大きい場合、ボタンは有効になります。myNumber
が 5 以下の場合は、ボタンは有効になりません。
UpdateNumber
ボタンを含むレイアウトに、次のように NumbersViewModel
の <data>
変数が含まれているとします。
<data> <variable name="NumbersViewModel" type="com.example.android.numbersapp.NumbersViewModel" /> </data>
レイアウト ファイルのボタンの ID は次のとおりとします。
android:id="@+id/update_number_button"
他にすべきことはありますか?該当するものをすべて選択してください。
NumbersViewModel
クラスで、数値を表すLiveData
変数myNumber
を定義します。また、myNumber
変数でTransform.map()
を呼び出すことで値が設定される変数も定義します。この変数は、数値が 5 より大きいかどうかを示すブール値を返します。
具体的には、ViewModel
に次のコードを追加します。
val myNumber: LiveData<Int>
val enableUpdateNumberButton = Transformations.map(myNumber) {
myNumber > 5
}
- XML レイアウトで、
update_number_button button
のandroid:enabled
属性をNumberViewModel.enableUpdateNumbersButton
に設定します。
android:enabled="@{NumbersViewModel.enableUpdateNumberButton}"
NumbersViewModel
クラスを使用するFragment
で、ボタンのenabled
属性にオブザーバーを追加します。
具体的には、Fragment
に次のコードを追加します。
// Observer for the enabled attribute
viewModel.enabled.observe(this, Observer<Boolean> { isEnabled ->
myNumber > 5
})
- レイアウト ファイルで、
update_number_button button
のandroid:enabled
属性を"Observable"
に設定します。
次のレッスンに進む:
このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。