fresh digitable

セミコロンたちが躍動する おいらのコードを 皆さんに 見せたいね

なぜAndroidアプリをMVVMでつくるといいのか

モチベーション

  • Androidでは画面のパーツは全部のオブジェクトへの参照を持つのでリークさせたくない
  • MVCとかMVPはCやPがVへの参照を持つので、とくにAndroidでは管理をきっちりやらなければならない
  • その一方で、ActivityやFragmentのライフサイクルが複雑なので管理をきっちりやるのは難しい
  • MVVMではVMはVへの参照を持たないので、MVCとかMVPとかよりは漏れにくいはず
  • そういう理由からAndroidアプリの開発にフィットしそうな設計ということで、公式から便利なライブラリがいろいろ出ているのでその恩恵を受けられる
  • MV-WhateverはMとVの関心ごとをいかに分離するかという話なのでRepositoryとかUseCaseとかとはまた別のはなしです

実装のはなし

  • MからVに向かってデータを伝えるにはObserver-Observableパターンを使う
    • ViewModelにObservableな変数を持たせてView側から観測する
  • ObservableFieldとかでもいいけどLiveDataを使うと画面が見えていないときはViewの更新が起きないようになっているので便利
    • ObservableIntとかObservableFloatは実際の値をJavaのプリミティブ型に保持しているので自動的にNonNullだしオブジェクトが作られないのでAnimatorの値を渡してもいいかもしれない
  • AndroidにはDataBindingの仕組みがありViewとLiveData(とかObaservableXxx)とを結びつけるコードを自動生成してくれる
    • DataBindingを使わずにMVVMをやろうとするのは大変つらいと思われるのでおすすめしない
  • ViewModelはPure Java(Kotlin)のクラスばっかりになるはずなのでテストが書きやすいし実行も速い
    • ViewとViewModelのプロパティが正しくバインドされているならば、ViewModelのプロパティをテストすることでその画面のテストを行うのと同じになるはず

Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6. Please specify proper '-jvm-target' option

ある日突然怒られたりするやつです。ライブラリを新しく入れたりアップデートしたときとか。

stackoverflow.com

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

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