iOSエンジニアのつぶやき

毎朝8:30に iOS 関連の技術について1つぶやいています。まれに釣りについてつぶやく可能性があります。

知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day21【Use LiveData to control button states編】

Android Kotlin Fundamentals: Use LiveData to control button states

学ぶこと

  • database 内の既存の睡眠品質レコードを変更する方法
  • LiveData を使用してボタンの状態を追跡する方法
  • イベントに応じて snackbar を表示する方法

すること

  • TrackMySleepQuality アプリを拡張して、品質評価を収集し、データベースに評価を追加して結果を表示します。
  • LiveData を使用して、snackbar の表示をトリガーします。
  • LiveData を使用して、ボタンを有効または無効にします。

Navigation を追加

コードを調べる

  1. Design Editor で、navigation.xml を開きます。SleepTrackerFragment から SleepQualityFragment への navigation path があり、SleepQualityFragment から SleepTrackerFragment へ戻る navigation path があることが分かります。

  1. navigation.xml のコードを調べます。特に sleepNightKey という名前の <argument> を探します。

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

Sleep-quality を追跡するための navigation を追加する

navigation graph には、SleepTrackerFragment から SleepQualityFragment へのパスとその逆のパスがすでに含まれています。ただし、ある Fragment から次の Fragment への Navigation を実装するクリックハンドラーはまだコーディングされていません。ここで、そのコードを ViewModel に追加します。

クリックハンドラーで、アプリを別の宛先に移動する時に変更される LiveData を設定します。fragment はこの LiveData を observe します。データが変更されると、fragment は宛先に移動し、完了したことを viewModel に通知します。これにより、状態変数がリセットーされます。

  1. SleepTrackerViewModel を開きます。ユーザーが Stop ボタンをタップした時にアプリが SleepQualityFragment に移動して品質評価を収集できるように、navigation を追加する必要があります。

  2. SleepTrackerViewModel で、アプリが SleepQualityFragment に移動する時に変更される LiveData を作成します。カプセル化を使用して、Gettable バージョンの LiveData のみを ViewModel に公開します。

このコードは、クラス本体の最上位のどこにでも配置できます。

private val _navigateToSleepQuality = MutableLiveData<SleepNight>()

val navigateToSleepQuality: LiveData<SleepNight>
   get() = _navigateToSleepQuality
  1. navigation をトリガーする変数をリセットするために doneNavigating() 関数を追加します。
fun doneNavigating() {
   _navigateToSleepQuality.value = null
}
  1. Stop ボタンのクリックハンドラー onStopTracking() で、SleepQualityFragment への navigation をトリガーします。関数の最後に _navigateToSleepQuality 変数を launch{} ブロック内の最後のものとして設定します。この変数は night に設定されていることに注意してください。この変数に値がある場合、アプリは SleepQualityFragment に移動し、night を渡します。
_navigateToSleepQuality.value = oldNight
  1. SleepTrackerFragment は、_navigateToSleepQuality を observe して、アプリがいつ Naivigate するかを認識できるようにする必要があります。SleepTrackerFragmentonCreateView() で、navigateToSleepQuality() のオブザーバーを追加します。androidx.lifecycle.Observer をインポートする必要があることに注意してください。
sleepTrackerViewModel.navigateToSleepQuality.observe(this, Observer {
})
  1. observer ブロック内で、current night の ID を navigate して渡し、doneNavigating() を呼び出します。androidx.navigation.fragment.findNavController をインポートする必要があります。
night ->
night?.let {
   this.findNavController().navigate(
           SleepTrackerFragmentDirections
                   .actionSleepTrackerFragmentToSleepQualityFragment(night.nightId))
   sleepTrackerViewModel.doneNavigating()
}
  1. アプリをビルドして実行します。Start ボタンをタップしてから Stop ボタンをタップすると、SleepQualityFragment 画面が表示されます。戻るにはシステムの Back Button を使用します。

睡眠の質を記録する

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

ViewModel と ViewModelFactory を作成する

  1. sleepquality パッケージで、SleepQualityViewModel.kt を作成または開きます。
  2. sleepNightKey とデータベースを引数として取る SleepQualityViewModel クラスを作成します。SleepTrackerViewModel の場合と同じように、factory から database を渡す必要があります。また、navigation から sleepNightKey を渡す必要があります。
class SleepQualityViewModel(
       private val sleepNightKey: Long = 0L,
       val database: SleepDatabaseDao) : ViewModel() {
}
  1. 上記と同じパターンを使用して、SleepTrackerFragment に戻るには、_navigateTpSleepTracker を宣言します。navigateToSleepTrackerdoneNavigating() を実装します。
private val _navigateToSleepTracker = MutableLiveData<Boolean?>()

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

fun doneNavigating() {
   _navigateToSleepTracker.value = null
}
  1. 使用する全ての睡眠品質の画像に対して、ワンクリックハンドラー onSetSleepQuality() を作成します。

前のコードラボと同じ Coroutine パターンを使用します:

  • viewModelScope で coroutine を起動する
  • sleepNightKey を使用して tonight を取得する
  • sleep quality をセットする
  • databse を更新する
  • navigation をトリガーする

以下のコードサンプルは、異なるコンテキストでのデータベース操作を除外するのではなく、クリックハンドラーで全ての作業を実行することに注意してください。

fun onSetSleepQuality(quality: Int) {
        viewModelScope.launch {
                val tonight = database.get(sleepNightKey) ?: return@launch
                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")
   }
}

SleepQualityFragment を更新する

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

レイアウトファイルを更新してアプリを実行します

  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. プロジェクトをクリーンアップして再構築します。これにより、binding object のエラーが解決されます。それ以外の場合は、キャッシュをクリアし(File > Invalidate Caches / Restart)、アプリを再構築します。

コントロールボタンの可視性とスナックバーの追加

このタスクでは、transformation maps を使用してボタンの可視性を管理し、ユーザが正しい選択のみを行えるようにする方法を学習します。同様の方法を使用して、すべてのデータがクリアされた後に分かりやすいメッセージを表示できます。

Button States を更新する

イデアは、最初はスタートボタンのみが有効になるようにボタンの状態を設定することです。つまり、clickable です。

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

  1. fragment_sleep_tracker.xml レイアウトファイルを開きます。
  2. 各ボタンに android:enabled プロパティを追加します。android:enabled プロパティは、ボタンが有効かどうかを示す bool 値です。

start_button:

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

stop_button:

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

clear_button:

android:enabled="@{sleepTrackerViewModel.clearButtonVisible}"
  1. SleepTrackerViewModel を開き、対応する3つの変数を作成します。各変数に、それをテストする Transformation を割り当てます。

  2. Start button は tonightnull の場合有効になります。

  3. Stop button は、tonightnull ではない場合に有効になります。
  4. Clear button は、nights、データベースにデータが含まれている場合にのみ有効になります。
val startButtonVisible = Transformations.map(tonight) {
   it == null
}
val stopButtonVisible = Transformations.map(tonight) {
   it != null
}
val clearButtonVisible = Transformations.map(nights) {
   it?.isNotEmpty()
}
  1. アプリを実行して、ボタンの挙動を確認してみてください。

Tip: 無効な View の外観を設定する enabled 属性は、visibility 属性と同じではありません。enabled 属性は、View が表示されているかどうかではなく、View が有効かどうかを決定するだけです。

無効になっている View にはデフォルトのスタイルが適用され、View がアクティブでないことを視覚的に表します。

ただし、View に background 属性または textColor 属性がある場合、View が無効になっている場合でも、View が表示される時にこれらの属性の値が使用されます。

有効状態と無効状態に使用する色を定義するには、テキストの色に ColorStateList を使用し、background color に StateListDrawable を使用します。

snackbar を使用してユーザに通知する

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

Snackbar の表示は UI タスクであり、Fragment で行われる必要があります。snackbar を表示することを決定するのは ViewModel で行われます。データがクリアされた時に snackbar を設定してトリガーするには、navigation のトリガーと同じ手法を使用できます。

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

val showSnackBarEvent: LiveData<Boolean>
   get() = _showSnackbarEvent
  1. 次に、doneShowingSnackbar() を実装します。
fun doneShowingSnackbar() {
   _showSnackbarEvent.value = false
}
  1. SleepTrackerFragmentonCreateView() で、observer を追加します。
sleepTrackerViewModel.showSnackBarEvent.observe(this, Observer { })
  1. observer ブロック内に、snackbar を表示し、すぐにイベントをリセットします。
   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. アプリをビルドして実行します。

まとめ

これらのパターンに注目すると、既存のアプリのコードを再利用できるため、コーディングスピードが向上します。

  • ViewModelViewModelFactory を作成し、datasource を設定します。
  • navigation をトリガーします。関心の分離を行うには、クリックハンドラーを ViewModel に配置し、navigation を Fragment に配置します。
  • LiveDataカプセル化を使用して、状態の変化を追跡し、応答します。
  • LiveData で transformations を使用します。
  • Singleton database を作成します。
  • database 操作用の Coroutine を設定します。

Navigation のトリガー

navigation file 内の fragment 間の可能な navigation path を定義します。ある Fragment から次の Fragment への navigation をトリガーする方法はいくつかあります。これらには以下が含まれます:

  • onClick ハンドラーを定義して、destination fragment への navigation をトリガーします。
  • ある Fragment から次の Fragment への navigation を有効にするには:
  • navigation が必要かどうかを記録する LiveData 値を定義します。
  • その LiveData 値にオブザーバーをアタッチします。
  • 次に、navigation をトリガーする必要がある時、または navigation が完了するたびに、コードはその値を変更します。

android:enabled attribute の設定

  • android:enabled 属性は TextView で定義され、Button を含むすべてのサブクラスに継承されます。

  • android:enabled 属性は、View を有効にするかどうかを決定します。"enabled" の意味はサブクラスによって異なります。例えば、有効になっていない EditText は、ユーザがテキストを編集できないようにし、有効になっていない Button は、ユーザがボタンをタップできないようにします。

  • enabled 属性は、visibility 属性と同じではありません。
  • transformation maps を使用して、別のオブジェクトまたは変数の状態に基づいてボタンの enabled 属性の値を設定できます。

このコードラボでカバーされているその他のポイント: - ユーザへの通知をトリガーするには、navigation のトリガーに使用するのと同じ手法を使用できます。 - Snackbar を使用してユーザに通知できます。

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com