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
を変えたりしたほうがいいのか、でも変えるとうまくいかないのかとか、いろんな試行錯誤はまだやっていないがとりあえず動いているっぽいという感じ。間違っていたりよくない使い方をしているようでしたら教えてください。