WeakReferenceに包みつつNonNullなプロパティとしてアクセスするためのデリゲートプロパティ
例えばこういうのを
private val _foo= WeakReference<FooActivity>(activity) private val foo: FooActivity get() = requireNotNull(_foo.get())
こんな感じで
private val foo: FooActivity by weakRef(activity)
使えるようにしたい。そんなときはデリゲートプロパティを使って、次のようなものを用意するとよい。
fun <T : Any> weakRef(t: T): ReadOnlyProperty<Any, T> = WeakRefProperty(t) private class WeakRefProperty<T : Any>(t: T) : ReadOnlyProperty<Any, T> { private val _t: WeakReference<T> = WeakReference(t) override fun getValue(thisRef: Any, property: KProperty<*>): T = requireNotNull(_t.get()) }
上の例で、activityじゃなくてそれが持っているViewにアクセスしたいというようなときは、lazyと組み合わせて、
fun <T : Any, R : Any> weakRef( t: T, lazyBlock: (T) -> R ): ReadOnlyProperty<Any, R> = WeakRefPropertyWithLazy(t, lazyBlock) private class WeakRefPropertyWithLazy<R : Any, T : Any>( r: R, lazyBlock: (R) -> T ) : ReadOnlyProperty<Any, T> { val rRef: R by weakRef(r) val tRef: WeakReference<T> by lazy { WeakReference(lazyBlock(rRef)) } override fun getValue(thisRef: Any, property: KProperty<*>): T = requireNotNull(tRef.get()) }
こんな感じのものを用意してやると
private val bar: View by weakRef(activity) { it.findViewById<VIew>(R.id.view_bar) }
このように書ける。NavControllerとかを取得するのもよい。
UdonRoad2/WeakRefProperty.kt at master · akihito104/UdonRoad2 · GitHub
fluxとかMVIみたいな構造のアプリを作ってみたかった
fluxとかMVIみたいなリアクティブ?な構造のアプリを作ってみたかったのでやってみた。Cycle.jsのドキュメントを参考にして自分なりにかみくだきながら作っていったので気持ちとしてはMVIのような感じだけど、実際に自分で作ったものが何なのかはわからない。少なくとも、すでに世にあるAndroidアプリのMVI実装とはぜんぜん違うようにみえる。
ざっくりとしたクラス図
実際に悩みながら分からないなりに書き進めて、ある程度形になったかなと思えるので、以下にポイントや雑感を書いていく。おおよその方針は決まったので、今後はもう少し考えて分割統治を進めたりうまくいっていないところを解消したりしていきたい。
- コールバックを呼ぶ代わりにUIイベントのオブジェクトをイベントバス(=EventDispatcher)に流して目的別に振り分け(=Actions)、ビジネスモデルの関数を引き当てる(=ViewStates)というざっくりとした理解
- 関心の分離の仕方は何となく好き
- 一つのイベントを受け取って一つの状態(個々のViewの状態の集合)に遷移させるという感じのものを目指したが、Databindingと一緒に使ってると結局最後にまたばらばらにするから一つの状態にまとめる処理いらなくない?と思ってやめた
- 画面遷移はイベントなので保存しない(RxJavaのObservable)、状態はデータなので保存する(LiveData)という考え方で組み立てている
- 中断関数を呼び出すのが面倒(中断関数にせずRxでスレッドを切り替えるのがスマートなんだろうけど@MainThreadとかの世界には何となく戻りたくない)
- ActionsもViewStatesもモジュールごとに分割できそう。現時点ではアプリルートのモジュールに全部書いている
- androidx.navigationを使った画面遷移をどうすればきれいに収められるか分からなかったので残念ながらフローが分岐している
- 画面遷移はNavHostFragmentの状態を遷移させることなので当初は自前で状態を管理しようとしていたが、NavControllerはバックスタックなども管理しているのでゆくゆくはそれも管理しなければならなくなって結局はNavControllerのようなものを作ることになるのでは…?と考えて全部androidx.navigationに寄せることにした
- 最初はViewStatesに入れれば一方向のフローになるかなと思ってやってみたんだけどkaptがメッセージ無しの謎のエラーを吐いてこれを解決できなかったので断念した
- テストについて:Repositoryはモックにしているが、それ以外は極力現物を使うようにしたらテストを準備するための処理が重複しまくってしまい面倒なことになってしまった。きれいにまとめる何かが必要
R8でコードシュリンクしつつInstrumentation Testをやる
いろいろハマってしんどかったので雑にまとめておく。
ことの顛末
- Instrumentation Testのテストが全然動かない。テストがないとか言われる
- Logcatをみると
DaggerTestAppComponent
をDaggerAppComponent
にキャストできませんとか言われている。そらそうよ - ちょっと書き換えると今度は
kotlin.reflect.KProperty0
がないとか言ってくる。なんでや - キャストするコードは書いていない。明にキャストしたり、安全キャスト(
as?
)をやったりしてみるがなぜかDaggerAppComponent
にキャストされようとしてクラッシュする。Kotlinバイトコードをデコンパイルしてみても特に変なところはない。dexにする段階で何か変わってしまったのか?と思ってapkファイルの中のバイトコードを確認すると、check-cast
という命令が追加されており、ここでDaggerAppComponent
型かどうかのチェックがされていた。 - R8の最適化が効いたのかな?と思って最適化させない命令(
-dontoptimize
)を付けると今度はkotlin.reflect.KProperty0
が無いといわれる。またか。だがdexのバイトコードにはcheck-cast
がなくなっていた。 - そうかそれならってことで今度は
-keep
でkotlin.reflect
以下のクラスを残すことにした。すると今度は別のクラスが無いという話になったので、一応効いているということが分かった。ないといわれたクラスを片っ端からkeepで残すことにしていったらテストが動くようになった。