この Codelab は、Android Kotlin の基礎コースの一部です。Codelab を順番に進めていくと、このコースを最大限に活用できます。すべてのコース Codelab は Android Kotlin の基礎 Codelab ランディング ページに掲載されています。
はじめに
この Codelab では、ViewModel
とフラグメントを組み合わせてナビゲーションを実装する方法を復習します。ここでの目標は、ViewModel
にナビゲートするタイミングのロジックを配置し、フラグメントとナビゲーション ファイル内でパスを定義することです。この目標を達成するには、ビューモデル、フラグメント、LiveData
、オブザーバーを使用します。
最後に、最小限のコードでボタンの状態をトラッキングするための優れた方法を示し、ユーザーがそのボタンをタップする意味がある場合にのみ各ボタンを有効にしてクリックできるようにする方法を紹介しました。
前提となる知識
以下について把握しておく必要があります。
- アクティビティ、フラグメント、ビューを使用した基本的なユーザー インターフェース(UI)の作成。
- フラグメント間を移動し、
safeArgs
を使用してフラグメント間でデータを渡す。 - モデル、モデル ファクトリ、変換、
LiveData
とそのオブザーバーを表示します。 Room
データベースの作成方法、データアクセス オブジェクト(DAO)を作成する方法、エンティティを定義する方法。- データベースの操作やその他の長時間実行タスクにコルーチンを使用する方法。
学習内容
- データベース内の既存の睡眠の質の記録を更新する方法。
LiveData
を使用してボタンの状態を追跡する方法- イベントに応じてスナックバーを表示する方法
演習内容
- TrackMySleepQuality アプリを拡張して品質評価を収集し、その評価をデータベースに追加して結果を表示します。
LiveData
を使用してスナックバーの表示をトリガーします。- ボタンを有効または無効にするには
LiveData
を使用します。
この Codelab では、TrackMySleepQuality アプリの睡眠品質の記録と最終的な UI を作成します。
アプリには、下図に示すように、フラグメントで表される画面が 2 つあります。
左側の最初の画面には、トラッキングを開始および停止するボタンがあります。画面には、ユーザーのすべての睡眠データが表示されます。[消去] ボタンをクリックすると、そのアプリがユーザーのために収集したすべてのデータが完全に削除されます。
右の 2 番目の画面では、睡眠の質の評価を選択しています。このアプリでは、レーティングは数値で表示されます。アプリでは、顔アイコンとそれに対応する数字の両方が表示されます。
ユーザーのフローは次のようになります。
- アプリを開くと、睡眠管理画面が表示されます。
- ユーザーが [開始] ボタンをタップします。これにより、開始時間が記録されて表示されます。[スタート] ボタンが無効になり、[停止] ボタンが有効になります。
- ユーザーが [停止] ボタンをタップします。終了時間を記録し、睡眠の質の画面を表示します。
- ユーザーが睡眠品質のアイコンを選択します。画面が終了し、トラッキング画面に睡眠時間と睡眠品質が表示されます。[停止] ボタンは無効になり、[開始] ボタンは有効になっています。アプリの準備はもう終わりです。
- データベースにデータが格納されている場合は常に、[クリア] ボタンが有効になります。ユーザーが [消去] ボタンをタップしても、すべてのデータが消去されずに消去されます。「よろしいですか?」というメッセージはありません。
このアプリは、完全なアーキテクチャのコンテキストに示すように、簡略化されたアーキテクチャを使用しています。アプリは次のコンポーネントのみを使用します。
- UI コントローラ
- モデルと
LiveData
を表示します - Room データベース
この Codelab は、フラグメントとナビゲーション ファイルを使用してナビゲーションを実装する方法を理解していることを前提としています。手間を省くために、このコードが多数提供されています。
ステップ 1: コードを検査する
- 開始するには、前回の Codelab の最後にある独自のコードを使って続けるか、スターター コードをダウンロードしてください。
- スターター コードで、
SleepQualityFragment
を検査します。このクラスは、レイアウトをインフレートし、アプリを取得して、binding.root
を返します。 - Design Editor で navigation.xml を開きます。
SleepTrackerFragment
からSleepQualityFragment
、SleepQualityFragment
からSleepTrackerFragment
へのナビゲーション パスが再び表示されています。 - navigation.xml のコードを確認します。特に、
sleepNightKey
という名前の<argument>
を探します。
ユーザーがSleepTrackerFragment
からSleepQualityFragment,
に移動すると、アプリは更新が必要な宿泊のsleepNightKey
をSleepQualityFragment
に渡します。
ステップ 2: 睡眠の質を記録するナビゲーションを追加する
ナビゲーション グラフには、SleepTrackerFragment
から SleepQualityFragment
へのパスと戻るボタンがすでに含まれています。ただし、あるフラグメントから次のフラグメントへのナビゲーションを実装するクリック ハンドラはまだコーディングされていません。このコードを ViewModel
に追加します。
クリック ハンドラで、アプリを別のデスティネーションに移動させるタイミングを変更する LiveData
を設定します。フラグメントはこの LiveData
を監視します。データが変更されると、フラグメントはデスティネーションに移動し、完了したことをビューモデルに伝えることで、状態変数をリセットします。
SleepTrackerViewModel
を開きます。ユーザーが [停止] ボタンをタップしたときに、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
へのナビゲーションをトリガーします。関数の最後で _navigateToSleepQuality
変数をlaunch{}
ブロック内の最後の要素として設定します。この変数はnight
に設定されています。この変数に値がある場合、アプリはSleepQualityFragment
に移動し、夜間を渡します。
_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)}"
- プロジェクトをクリーンアップして再ビルドします。これにより、バインディング オブジェクトのエラーが解決します。それ以外の場合は、キャッシュ([ファイル] > [キャッシュを無効にする] / [再起動])をクリックしてアプリを再ビルドします。
これで、コルーチンを使用して完全な Room
データベース アプリを作成しました。
これで、アプリが正常に機能するようになりました。[起動] と [停止] を何回でもタップできます。[停止] をタップすると、睡眠のクオリティを入力できます。ユーザーが [消去] をタップすると、データはすべてバックグラウンドでサイレントに消去されます。ただし、ボタンはすべて常に有効でクリック可能であるため、アプリは機能しなくなるものの、ユーザーは不眠の睡眠を覚えられるようになります。
この最後のタスクでは、変換マップを使用してボタンの公開設定を管理し、ユーザーが正しい選択を行えるようにする方法を学習します。データをすべて消去した後に、同様の方法でわかりやすいメッセージを表示できます。
ステップ 1: ボタンの状態を更新する
ボタンの状態は、最初は [Start] ボタンのみが有効(つまりクリック可能)になるように設定してください。
ユーザーが [開始] をタップすると、[停止] ボタンが有効になり、[開始] ボタンが有効になりません。[クリア] ボタンは、データベースにデータがある場合にのみ有効になります。
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
を有効にするかどうかを決定します。「enabled」の意味はサブクラスによって異なります。たとえば、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
Button
を有効にする(クリック可能)かどうかは、LiveData
を使用して変更できます。アプリで 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 ランディング ページをご覧ください。