fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

ObservableでないプロパティをKotlinのChannelで定期的に通知させる

コールバックがなくて値が変わった時にそのことがわからないようなプロパティを観測可能にしたい。KotlinのChannelの機能を使ってそれっぽい仕組みを作ってみた。Channelは現時点(kotlinx-coroutine 1.1.1)ではExperimental。

Androidで動画や音楽を再生するときに使うMediaPlayerは再生の進捗状況を外部に通知する機能がない。そこでMediaPlayer.currentPositionを定期的に読み取ってChannelに流すことにする。そのChannelを購読してプログレスバーや残り時間のテキストを更新する。

MediaPlayerを保持する関係上、ActivityFragmentCoroutineScopeを実装して、そのなかで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()
    }
}

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