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

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

  1. 開始するには、前回の Codelab の最後にある独自のコードを使って続けるか、スターター コードをダウンロードしてください。
  2. スターター コードで、SleepQualityFragment を検査します。このクラスは、レイアウトをインフレートし、アプリを取得して、binding.root を返します。
  3. Design Editor で navigation.xml を開きます。SleepTrackerFragment から SleepQualityFragmentSleepQualityFragment から SleepTrackerFragment へのナビゲーション パスが再び表示されています。



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

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

ステップ 2: 睡眠の質を記録するナビゲーションを追加する

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

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

  1. SleepTrackerViewModel を開きます。ユーザーが [停止] ボタンをタップしたときに、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 へのナビゲーションをトリガーします。関数の最後で _navigateToSleepQuality 変数を launch{} ブロック内の最後の要素として設定します。この変数は night に設定されています。この変数に値がある場合、アプリは SleepQualityFragment に移動し、夜間を渡します。
_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. プロジェクトをクリーンアップして再ビルドします。これにより、バインディング オブジェクトのエラーが解決します。それ以外の場合は、キャッシュ([ファイル] > [キャッシュを無効にする] / [再起動])をクリックしてアプリを再ビルドします。

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

これで、アプリが正常に機能するようになりました。[起動] と [停止] を何回でもタップできます。[停止] をタップすると、睡眠のクオリティを入力できます。ユーザーが [消去] をタップすると、データはすべてバックグラウンドでサイレントに消去されます。ただし、ボタンはすべて常に有効でクリック可能であるため、アプリは機能しなくなるものの、ユーザーは不眠の睡眠を覚えられるようになります。

この最後のタスクでは、変換マップを使用してボタンの公開設定を管理し、ユーザーが正しい選択を行えるようにする方法を学習します。データをすべて消去した後に、同様の方法でわかりやすいメッセージを表示できます。

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

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

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

  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. SleepTrackerViewModelonClear() メソッドでイベントをトリガーします。これを行うには、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 を有効にするかどうかを決定します。「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 に移動する ViewonClick ハンドラをコードで定義してください。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 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 ランディング ページをご覧ください。