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

animatorが終わった時の挙動を変えたかった

AndroidValueAnimatorでワンショットのアニメーションを実装していたところ、動いている最中に止めたくなった時に

  • 最初の状態に戻したい
  • 最後の状態まですっ飛ばしたい

ということがあって少し考えた。

AndroidAnimatorクラスにはcancel()end()というメソッドがあってどちらもアニメーションが止まるのだが、

  • cancel()は呼ばれた時の状態で止まる
  • end()は最後の状態になる

という感じになっていた*1ので、cancel()のほうの挙動を変えるためにAnimatorListenerをラップしてAnimatorListener.onAnimationCancel()の中で最初の状態に戻すようなクラスを作った。

github.com

ワンショットのアニメーションなので、アニメーションが止まった時にリスナをremoveする処理もついでに入れた。最初コードを読んでいるとcancel()を呼んだ時にもAnimatorListener.onAnimationEnd()が呼ばれそうに見えたのでonAnimationCancel()が呼ばれたときにフラグを立ててonAnimationEnd()の中身が実行されないようにしていたんだけど、実際にはonAnimationEnd()は呼ばれなかったので最終的にフラグは使わないことになった。今後いろんなケースに対応していくなかでキャンセルフラグを復活させることがあるかもしれない。

*1:API Level 23のエミュレータで確認

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のテストもいくらか楽になるかもしれない。