Android Kotlin の基礎 06.3: LiveData を使用してボタンの状態を制御する

この 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: コードを検査する

  1. まず、前回の Codelab の最後に作成したコードを使用するか、スターター コードをダウンロードします。
  2. スターター コードで、SleepQualityFragment を調べます。このクラスは、レイアウトを拡張し、アプリケーションを取得して、binding.root を返します。
  3. デザイン エディタで navigation.xml を開きます。SleepTrackerFragment から SleepQualityFragment へのナビゲーション パスがあり、SleepQualityFragment から SleepTrackerFragment に戻るパスがあることがわかります。



  4. navigation.xml のコードを検査します。特に、sleepNightKey という名前の <argument> を探します。

    ユーザーが SleepTrackerFragment から SleepQualityFragment, に移動すると、アプリは更新が必要な夜間の sleepNightKeySleepQualityFragment に渡します。

ステップ 2: 睡眠の質のトラッキングのナビゲーションを追加する

ナビゲーション グラフには、SleepTrackerFragment から SleepQualityFragment へのパスと、そこから戻るパスがすでに含まれています。ただし、あるフラグメントから次のフラグメントへのナビゲーションを実装するクリック ハンドラはまだコード化されていません。このコードを ViewModel に追加します。

クリック ハンドラで、アプリが別のデスティネーションに移動するタイミングで変更される LiveData を設定します。フラグメントはこの LiveData を監視します。データが変更されると、フラグメントは宛先に移動し、ビューモデルに完了したことを伝えます。これにより、状態変数がリセットされます。

  1. SleepTrackerViewModel を開きます。ユーザーが [Stop] ボタンをタップしたときに、アプリが SleepQualityFragment に移動して品質評価を収集するように、ナビゲーションを追加する必要があります。
  2. SleepTrackerViewModel で、アプリが SleepQualityFragment に移動するタイミングで変更される LiveData を作成します。カプセル化を使用して、LiveData の取得可能なバージョンのみを ViewModel に公開します。

    このコードは、クラス本体の最上位の任意の場所に配置できます。
private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. ナビゲーションをトリガーする変数をリセットする doneNavigating() 関数を追加します。
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. [停止] ボタンのクリック ハンドラ onStopTracking() で、SleepQualityFragment へのナビゲーションをトリガーします。関数の末尾で、launch{} ブロック内の最後の要素として _navigateToSleepQuality 変数を設定します。この変数は night に設定されています。この変数に値がある場合、アプリは SleepQualityFragment に移動し、night.
    を渡します。
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment は、アプリがナビゲーションを行うタイミングを把握できるように、_navigateToSleepQuality を監視する必要があります。SleepTrackerFragmentonCreateView() で、navigateToSleepQuality() のオブザーバーを追加します。このインポートは曖昧であるため、androidx.lifecycle.Observer をインポートする必要があります。
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})

  1. オブザーバー ブロック内で、ナビゲートして現在の夜の ID を渡し、doneNavigating() を呼び出します。インポートがあいまいな場合は、androidx.navigation.fragment.findNavController をインポートします。
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. アプリをビルドして実行します。[Start] をタップしてから [Stop] をタップすると、SleepQualityFragment 画面が表示されます。戻るには、システムの [戻る] ボタンを使用します。

このタスクでは、睡眠の質を記録し、睡眠トラッカー フラグメントに戻ります。表示は自動的に更新され、更新された値がユーザーに表示されます。ViewModelViewModelFactory を作成し、SleepQualityFragment を更新する必要があります。

ステップ 1: ViewModel と ViewModelFactory を作成する

  1. sleepquality パッケージで、SleepQualityViewModel.kt を作成または開きます。
  2. sleepNightKey とデータベースを引数として受け取る SleepQualityViewModel クラスを作成します。SleepTrackerViewModel の場合と同様に、ファクトリから database を渡す必要があります。ナビゲーションから sleepNightKey も渡す必要があります。
class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. SleepQualityViewModel クラス内で、JobuiScope を定義し、onCleared() をオーバーライドします。
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)

override fun onCleared() {
   super.onCleared()
   viewModelJob.cancel()
}
  1. 上記と同じパターンを使用して SleepTrackerFragment に戻るには、_navigateToSleepTracker を宣言します。navigateToSleepTrackerdoneNavigating() を実装します。
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

val navigateToSleepTracker: LiveData<Boolean?>
   get() = _navigateToSleepTracker

fun doneNavigating() {
   _navigateToSleepTracker.value = null
}
  1. 使用するすべての睡眠の質の画像に対して、クリック ハンドラ 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
        }
    }
  1. 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 を更新する

  1. SleepQualityFragment.kt を開きます。
  2. onCreateView()application を取得したら、ナビゲーションに付属する arguments を取得する必要があります。これらの引数は SleepQualityFragmentArgs にあります。バンドルから抽出する必要があります。
val arguments = SleepQualityFragmentArgs.fromBundle(arguments!!)
  1. 次に、dataSource を取得します。
val dataSource = SleepDatabase.getInstance(application).sleepDatabaseDao
  1. dataSourcesleepNightKey を渡してファクトリを作成します。
val viewModelFactory = SleepQualityViewModelFactory(arguments.sleepNightKey, dataSource)
  1. ViewModel 参照を取得します。
val sleepQualityViewModel =
       ViewModelProviders.of(
               this, viewModelFactory).get(SleepQualityViewModel::class.java)
  1. バインディング オブジェクトに ViewModel を追加します。(バインディング オブジェクトでエラーが発生した場合は、現時点では無視してください)。
binding.sleepQualityViewModel = sleepQualityViewModel
  1. オブザーバーを追加します。プロンプトが表示されたら、androidx.lifecycle.Observer をインポートします。
sleepQualityViewModel.navigateToSleepTracker.observe(this, Observer {
   if (it == true) { // Observed state is true.
       this.findNavController().navigate(
               SleepQualityFragmentDirections.actionSleepQualityFragmentToSleepTrackerFragment())
       sleepQualityViewModel.doneNavigating()
   }
})

ステップ 3: レイアウト ファイルを更新してアプリを実行する

  1. fragment_sleep_quality.xml レイアウト ファイルを開きます。<data> ブロックに SleepQualityViewModel の変数を追加します。
 <data>
       <variable
           name="sleepQualityViewModel"
           type="com.example.android.trackmysleepquality.sleepquality.SleepQualityViewModel" />
   </data>
  1. 6 つの睡眠の質を表す画像それぞれに、次のようなクリック ハンドラを追加します。画質評価を画像に一致させます。
android:onClick="@{() -> sleepQualityViewModel.onSetSleepQuality(5)}"
  1. プロジェクトをクリーンして再ビルドします。これで、バインディング オブジェクトのエラーが解決されます。それ以外の場合は、キャッシュをクリア([File] > [Invalidate Caches / Restart])してアプリを再ビルドします。

これで、コルーチンを使用して、完全な Room データベース アプリを作成しました。

これで、アプリは正常に動作します。ユーザーは [開始] と [停止] を何度でもタップできます。[停止] をタップすると、睡眠の質を入力できます。ユーザーが [クリア] をタップすると、すべてのデータがバックグラウンドで自動的にクリアされます。ただし、すべてのボタンが常に有効でクリック可能であるため、アプリが壊れることはありませんが、ユーザーが不完全な睡眠記録を作成できてしまいます。

この最後のタスクでは、変換マップを使用してボタンの表示 / 非表示を管理し、ユーザーが正しい選択肢のみを選択できるようにする方法について説明します。同様の方法で、すべてのデータが消去された後にフレンドリーなメッセージを表示できます。

ステップ 1: ボタンの状態を更新する

ボタンの状態を設定して、最初は [Start] ボタンのみが有効(クリック可能)になるようにします。

ユーザーが [開始] をタップすると、[停止] ボタンが有効になり、[開始] は無効になります。[Clear] ボタンは、データベースにデータがある場合にのみ有効になります。

  1. fragment_sleep_tracker.xml レイアウト ファイルを開きます。
  2. 各ボタンに android:enabled プロパティを追加します。android:enabled プロパティは、ボタンが有効かどうかを示すブール値です。(有効なボタンはタップできますが、無効なボタンはタップできません)。プロパティに、後で定義する状態変数の値を指定します。

start_button:

android:enabled="@{sleepTrackerViewModel.startButtonVisible}"

stop_button:

android:enabled="@{sleepTrackerViewModel.stopButtonVisible}"

clear_button:

android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
  1. SleepTrackerViewModel を開き、対応する 3 つの変数を作成します。各変数に、その変数をテストする変換を割り当てます。
  • tonightnull の場合、[開始] ボタンは有効になっている必要があります。
  • tonightnull でない場合、[停止] ボタンは有効にする必要があります。
  • クリア ボタンは、nights、つまりデータベースに睡眠記録が含まれている場合にのみ有効にする必要があります。
val startButtonVisible = Transformations.map(tonight) {
   it == null
}
val stopButtonVisible = Transformations.map(tonight) {
   it != null
}
val clearButtonVisible = Transformations.map(nights) {
   it?.isNotEmpty()
}
  1. アプリを実行し、ボタンを試してみます。

ステップ 2: スナックバーを使用してユーザーに通知する

ユーザーがデータベースをクリアしたら、Snackbar ウィジェットを使用して確認を表示します。スナックバーは、画面下部のメッセージを通じて操作に関する簡単なフィードバックを提供します。スナックバーは、タイムアウト後、画面の別の場所でユーザーが操作を行った後、またはユーザーがスナックバーを画面外にスワイプした後に消えます。

スナックバーの表示は UI タスクであり、フラグメントで行う必要があります。スナックバーを表示するかどうかは ViewModel で決定されます。データがクリアされたときにスナックバーを設定してトリガーするには、ナビゲーションをトリガーする場合と同じ手法を使用します。

  1. SleepTrackerViewModel で、カプセル化されたイベントを作成します。
private var _showSnackbarEvent = MutableLiveData<Boolean>()

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. 次に、doneShowingSnackbar() を実装します。
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. SleepTrackerFragmentonCreateView() にオブザーバーを追加します。
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. オブザーバー ブロック内で、スナックバーを表示し、イベントを直ちにリセットします。
   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()
   }
  1. SleepTrackerViewModel で、onClear() メソッドのイベントをトリガーします。これを行うには、launch ブロック内のイベント値を true に設定します。
_showSnackbarEvent.value = true
  1. アプリをビルドして実行します。

Android Studio プロジェクト: TrackMySleepQualityFinal

このアプリに睡眠管理を実装することは、新しいキーでよく知られた曲を演奏するようなものです。詳細は異なりますが、このレッスンで以前の Codelab で行ったことの基本的なパターンは同じです。これらのパターンを認識しておくと、既存のアプリのコードを再利用できるため、コーディングが大幅に高速化されます。このコースでこれまで使用したパターンをいくつか紹介します。

  • ViewModelViewModelFactory を作成し、データソースを設定します。
  • ナビゲーションをトリガーします。関心を分離するため、クリック ハンドラはビューモデルに、ナビゲーションはフラグメントに配置します。
  • 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 で、LiveDatagotoBlueFragment を定義します。
  • RedFragmentgotoBlueFragment の値を確認します。observe{} コードを実装して、必要に応じて BlueFragment に移動し、gotoBlueFragment の値をリセットして移動が完了したことを示します。
  • アプリが RedFragment から BlueFragment に移動する必要があるたびに、コードが gotoBlueFragment 変数をナビゲーションをトリガーする値に設定するようにします。
  • ユーザーがクリックして BlueFragment に移動する ViewonClick ハンドラがコードで定義されていることを確認します。この 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 buttonandroid: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 buttonandroid:enabled 属性を "Observable" に設定します。

次のレッスンに進む: 7.1 RecyclerView の基礎

このコースの他の Codelab へのリンクについては、Android Kotlin の基礎の Codelab のランディング ページをご覧ください。