iOSエンジニアのつぶやき

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

知識ゼロからの Kotlin Android アプリリリースへの軌跡 / Day18【LiveData transformations編】

学ぶこと

  • LiveDatTransformations を使用する方法

すること

  • ゲームを終了するタイマーを追加します。
  • Transformations.map() を使用して、ある LiveData を別の LiveData に変換します。

アプリの概要

今回は、前回のコードラボに続き、スコアの上に表示される一分間のカウントダウンタイマーを追加して、GuessTheWord アプリを改善します。カウントダウンが 0 に到達すると、タイマーはゲームを終了します。

また、transformation を使用して、経過時間 LiveData オブジェクトをタイマー文字列 LiveData オブジェクトに format します。変換された LiveData は、timer's text view のデータバインディングソースです。

Timer を追加

このタスクでは、アプリに CountDownTimer を追加します。word list が空の時にゲームが終了するのではなく、タイマーが終了するとゲームが終了します。Android には、タイマーの実装に使用する CountDownTimer という urility クラスが用意されています。

GameViewModel にタイマーのロジックを追加して、configuration の変更中にタイマーが破棄されないようにします。fragment には、タイマーが作動した時に timer text view を更新するコードが含まれています。

GameViewModel クラスに次の手順を実装します:

  1. タイマー定数を保持する companion オブジェクトを作成します。
companion object {

   // Time when the game is over
   private const val DONE = 0L

   // Countdown time interval
   private const val ONE_SECOND = 1000L

   // Total time for the game
   private const val COUNTDOWN_TIME = 60000L

}
  1. timer のカウントダウン時間を保存するには、_currentTime という MutableLiveData メンバー変数と backing property currentTime を追加します。
// Countdown time
private val _currentTime = MutableLiveData<Long>()
val currentTime: LiveData<Long>
   get() = _currentTime
  1. CountDownTimertimer と呼ばれる private メンバー変数を追加します。次の手順で初期化エラーを解決します。
private val timer: CountDownTimer
  1. init ブロック内で、タイマーを初期化して開始します。合計時間 COUNTDOWN_TIME を渡します。time interval には、ONE_SECOND を使用します。コールバックメソッド onTick() および onFinish() をオーバーライドして、タイマーを開始します。
// Creates a timer which triggers the end of the game when it finishes
timer = object : CountDownTimer(COUNTDOWN_TIME, ONE_SECOND) {

   override fun onTick(millisUntilFinished: Long) {
       
   }

   override fun onFinish() {
       
   }
}

timer.start()
  1. onTick() コールバックメソッドを実装します。これは interval または tick ごとに呼び出されます。渡されたパラメーター milisUntilFinished を使用して、_currentTime を更新します。milisUntilFinished は、タイマーが終了するまでの時間(ミリ秒単位)です。milisUntilFinishedseconds に変換し、それを _currentTime に割り当てます。
override fun onTick(millisUntilFinished: Long)
{
   _currentTime.value = millisUntilFinished/ONE_SECOND
}
  1. onFinish() コールバックメソッドは、タイマーが終了した時に呼び出されます。onFinish() を実装して、_currentTime を更新し、ゲーム終了イベントをトリガーします。
override fun onFinish() {
   _currentTime.value = DONE
   onGameFinish()
}
  1. nextWord() メソッドを更新して、ゲームを終了する代わりに、リストが空の時に単語リストをリセットします。
private fun nextWord() {
   // Shuffle the word list, if the list is empty 
   if (wordList.isEmpty()) {
       resetList()
   } else {
   // Remove a word from the list
   _word.value = wordList.removeAt(0)
   }
}
  1. onCleared() メソッド内で、メモリリークを回避するためにタイマーをキャンセルします。log ステートメントは不要になったため、削除できます。onCleared() メソッドは、ViewModel が破棄される前に呼び出されます。
override fun onCleared() {
   super.onCleared()
   // Cancel the timer
   timer.cancel()
}
  1. アプリを実行してゲームをプレイします。60秒待つと、ゲームは自動的に終了します。ただし、タイマーテキストは画面に表示されません。次にそれを修正します。

LiveData の Transformation を追加する

Transformation.map()) メソッドは、ソース LiveData でデータを操作を実行し、結果の LiveData オブジェクトを返す方法を提供します。これらの変換は、オブザーバーが返された LiveData オブジェクトを監視していない限り計算されません。

このメソッドは、ソース LiveData と関数をパラメーターとして受け取ります。この関数は、ソース LiveData を操作します。

Note: Transformation.map() に渡されるラムダ関数はメインスレッドで実行されるため、長時間実行されるタスクは含めないでください。

このタスクでは、経過時間 LiveData オブジェクトを MM:SS 形式の新しい文字列 LiveData オブジェクトにフォーマットします。また、フォーマットされた経過時間を画面に表示します。

game_fragment.xml レイアウトファイルには、すでに timer text view が含まれています。これまでのところ、text view には表示するテキストがないため、タイマーテキストは表示されていません。

  1. GameViewModel クラスで、currentTimeインスタンス化した後、currentTimeString という名前の新しい LiveData オブジェクトを作成します。このオブジェクトは、currentTime のフォーマットされた文字列バージョン用です。

  2. Transformation.map() を使用して currentTimeString を定義します。currentTime とラムダ関数を渡して時間をフォーマットします。DataUtils.formatElapsedTime()) ユーティリティーメソッドを使用してラムダ関数を実装できます。このメソッドは、long ミリ秒を要し、MM:SS 文字列形式にフォーマットします。

// The String version of the current time
val currentTimeString = Transformations.map(currentTime) { time ->
   DateUtils.formatElapsedTime(time)
}
  1. game_fragment.xml ファイルの timer text view で、text attribute を gameViewModelcurrentTimeString にバインドします。
<TextView
   android:id="@+id/timer_text"
   ...
   android:text="@{gameViewModel.currentTimeString}"
   ... />
  1. アプリを実行してゲームをプレイします。タイマーテキストは1秒に一回更新されます。全ての word を循環しても、ゲームは終了しないことに注目してください。タイマーが切れるとゲームが終了します。

まとめ

Transforming LiveData

  • LiveData の結果を変換したい場合があります。例えば、Date string を "hours:mins:seconds" としてフォーマットしたり、リスト自体を返すのではなく、リスト内のアイテムの数を返したりすることができます。LiveDataTransformations() クラスのヘルパーメソッドを使用します。

  • Transformations.map()) メソッドは、LiveData でデータ操作を実行し、別の LiveData オブジェクトを返す簡単な方法を提供します。推奨される方法は、Transformations クラスを使用するデータフォーマットロジックを UI データと共に ViewModel に配置することです。

変換の結果を TextView に表示する

  • ソースデータが ViewModelLiveData として定義されていることを確認してください。
  • newResult などの変数を定義します。Transformation.map() を使用して transformation を実行し、結果を変数に返します。
val newResult = Transformations.map(someLiveData) { input ->
   // Do some transformation on the input live data
   // and return the new value
}
  • TextView を含むレイアウトファイルが ViewModel<data> 変数を宣言していることを確認してください。
<data>
   <variable
       name="MyViewModel"
       type="com.example.android.something.MyViewModel" />
</data>
  • レイアウトファイルで、TextViewtext attribute を ViewModelnewResultバインディングに設定します。例えば:
android:text="@{SomeViewModel.newResult}"

Formatting dates

  • DateUtils.formatElapsedTime()) ユーティリティメソッドは、long ミリ秒数を要し、MM:SS string format を使用するように数値をフォーマットします。

その他の記事

yamato8010.hatenablog.com

yamato8010.hatenablog.com

yamato8010.hatenablog.com