ObservableでないプロパティをKotlinのChannelで定期的に通知させる
コールバックがなくて値が変わった時にそのことがわからないようなプロパティを観測可能にしたい。KotlinのChannel
の機能を使ってそれっぽい仕組みを作ってみた。Channel
は現時点(kotlinx-coroutine 1.1.1)ではExperimental。
Androidで動画や音楽を再生するときに使うMediaPlayer
は再生の進捗状況を外部に通知する機能がない。そこでMediaPlayer.currentPosition
を定期的に読み取ってChannel
に流すことにする。そのChannel
を購読してプログレスバーや残り時間のテキストを更新する。
MediaPlayer
を保持する関係上、Activity
やFragment
にCoroutineScope
を実装して、そのなかでReceiveChannel
を作ったりそれを購読するためのコルーチンを起動しなければならない。とりあえず次のようなCoroutineScope
を実装してActivity
で使えるようにする。
class MovieMediaCoroutineScope : CoroutineScope, LifecycleObserver { private lateinit var job: Job override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) fun onCreated() { if (!this::job.isInitialized || job.isCancelled) { job = Job() } } @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) fun onDestroy() { job.cancel() } }
あとはMediaPlayer
をproduce {}
でラップしてReceiveChannel
をつくり、launch
して流れてくるたびにView
を更新する。
@ExperimentalCoroutinesApi private fun MediaPlayer.setupProgress(progressText: TextView, progressBar: ProgressBar) { val currentPosStream: ReceiveChannel<Int> = produce { var oldPosition = -1 while (isActive) { if (oldPosition != currentPosition) { send(currentPosition) oldPosition = currentPosition } kotlinx.coroutines.delay(200) } } progressBar.max = duration val timeElapseFormat = getString(R.string.media_remain_time) launch { for (pos in currentPosStream) { val remain: Long = (progressBar.max - pos).toLong() val minutes = TimeUnit.MILLISECONDS.toMinutes(remain) val seconds = TimeUnit.MILLISECONDS.toSeconds(remain - TimeUnit.MINUTES.toMillis(minutes)) progressBar.progress = pos progressText.text = String.format(timeElapseFormat, minutes, seconds) } } }
値が変わった時だけ流したりとか、経過時間を表示するか残り時間を表示するかはお好みで。Dispatcher
を変えたりしたほうがいいのか、でも変えるとうまくいかないのかとか、いろんな試行錯誤はまだやっていないがとりあえず動いているっぽいという感じ。間違っていたりよくない使い方をしているようでしたら教えてください。
animatorが終わった時の挙動を変えたかった
AndroidのValueAnimator
でワンショットのアニメーションを実装していたところ、動いている最中に止めたくなった時に
- 最初の状態に戻したい
- 最後の状態まですっ飛ばしたい
ということがあって少し考えた。
androidのanimation、cancel呼んだら最初の状態に戻ってほしいしend呼んだら途中をすっ飛ばして最後の状態になってほしいんだけど何かそういう感じの便利なやつありませんか
— ありがとう日清カレーメシ (@akihito104) July 11, 2019
AndroidのAnimator
クラスにはcancel()
とend()
というメソッドがあってどちらもアニメーションが止まるのだが、
cancel()
は呼ばれた時の状態で止まるend()
は最後の状態になる
という感じになっていた*1ので、cancel()
のほうの挙動を変えるためにAnimatorListener
をラップしてAnimatorListener.onAnimationCancel()
の中で最初の状態に戻すようなクラスを作った。
ワンショットのアニメーションなので、アニメーションが止まった時にリスナをremoveする処理もついでに入れた。最初コードを読んでいるとcancel()
を呼んだ時にもAnimatorListener.onAnimationEnd()
が呼ばれそうに見えたのでonAnimationCancel()
が呼ばれたときにフラグを立ててonAnimationEnd()
の中身が実行されないようにしていたんだけど、実際にはonAnimationEnd()
は呼ばれなかったので最終的にフラグは使わないことになった。今後いろんなケースに対応していくなかでキャンセルフラグを復活させることがあるかもしれない。
Retrofitで実装したREST APIのinterfaceクラスを何かで包んで使ったらいい感じ
Retrofitを使って定義したREST APIのinterfaceは何かで包んでからRepositoryとかに渡すのがいい、という話。Retrofitに限らず何か特定ののREST APIにアクセスするライブラリを使うときにも有効ではないかと思う。そのようにするモチベーションを次の通り述べる。私はRestClientみたいな名前のクラスに包んでいる。
共通の処理を注入する
REST APIへのアクセスはいろんな画面で起きるので、エラーレスポンスのハンドリングも素直にやると画面ごとに実装しなければならなくなる。 例えば、レスポンスが返ってこないときはいつも決まったメッセージを出すとか、そういった共通の処理はなにかの形でまとめたい。 そのような用途のために、エラーレスポンスが返ってきたときの共通処理や、エラーレスポンスをイベントに変換してどこかに流すPublisherやChannel的なものを用意してRestClientに注入する。
リストデータの取得方法を隠蔽する
リストデータを取得するREST APIでは2ページ目以降を取得するときにトークンが必要になることがある。 このトークンは1ページ目のレスポンスの中にリストデータと一緒に含まれている。 2ページ目を取得するときまでは何らかの形で覚えておかなければならない。 そのような時、メモリ上に保持しておけばいいというような場合はRestClientで保持していればよい。 RestClientの利用者はトークンを意識することなく次のページを取得できる。次のページを取得する方法を隠蔽しているので、REST側がランダムアクセス的なインタフェースに変わったとしても(あるいはランダムアクセス的なインタフェースからトークン方式に変わったとしても)RestClientの利用者は処理を変える必要がない。 トークンをシリアライズしたいというときには一工夫する必要があるかもしれない。
ライブラリとアプリとの境界を規定する
シンプルに見ればライブラリとアプリとの間にレイヤーを挟む格好になるので、腐敗防止層的な役割もやってくれる(Retrofitが腐敗してしまうのは考えたくないけど)。 また、REST API側が開発中で完成していない場合や、まだバギーな時も代わりの値を返すようなものを差し込みやすいかもしれない。Repositoryのテストもいくらか楽になるかもしれない。