fresh digitable

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

fluxとかMVIみたいな構造のアプリを作ってみたかった その2

以前書いたこの記事

akihito104.hatenablog.com

の中で、

最初はViewStatesに入れれば一方向のフローになるかなと思ってやってみたんだけどkaptがメッセージ無しの謎のエラーを吐いてこれを解決できなかったので断念した

というようなことを書いた。今回はこれを解決したのでその顛末を書く。

ここで困っていることをもう少しだけ詳しく説明すると、NavControllerを持っているNavigationDelegateクラスをViewStatesに注入したくてそのようにコードを書いたら、daggerがコード生成をやっている最中に謎のエラーを吐いてうまくいかなかった、という感じ。エラーメッセージはタスクの実行時にstacktraceオプションを付けなければ見ることができなかったし、スタックトレースをさかのぼった先にあるエラーメッセージに

java.lang.IllegalArgumentException: no expression found for BindingRequest...

と書いてあってもどこがどう悪いのかさっぱり分からんので詰んだと思っていた。しかしながら詰んだといっても投げ出すのも癪なので、依存関係のグラフを描いてくれてそのうえダメなところを図示してくれるツールとか無いんだろうかと探してみたところ、scabbardというツールを見つけた。

arunkumar9t2.github.io

ダメなところを教えてくれるというのは無理なようだが*1、グラフをインタラクティブSVGで出力してくれるというもの*2で、依存関係の全体像をつかむのにすごく役立った。

私のケースでは、ビルドは失敗しているものの図の出力はなぜかできていたので、コードを少しいじってはビルドして図を眺めるというのをしばらく繰り返した。そんなことをやっているうちに、あるコンテキストでは注入しているオブジェクトが、また別のコンテキストでは注入できていないということに気が付いて雑にprovide関数を実装したところビルドが通るようになった。図で描くとこんな感じ。

scabbardによる依存グラフ。右側のコンポーネントからは注入できていたが左側からはできていなかったのでビルドが通らなかった
scabbardによる依存グラフ。右側のコンポーネントからは注入できていたが左側からはできていなかったのでビルドが通らなかった

これでめでたく依存関係を整理できたので、当初目指していた構造になった。

https://user-images.githubusercontent.com/9658489/91858210-d8d6c680-eca3-11ea-802f-bb4a9e01ce2a.png

その時のクソデカPRくん: https://github.com/akihito104/UdonRoad2/pull/88

引き続き実装を進めていく。

*1:コンパイルが通らなかったら図が出力されない

*2:図中のコンポーネントをクリックするとそのコンポーネント内の依存グラフを見ることができる

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実装とはぜんぜん違うようにみえる。

github.com

ざっくりとしたクラス図

実際に悩みながら分からないなりに書き進めて、ある程度形になったかなと思えるので、以下にポイントや雑感を書いていく。おおよその方針は決まったので、今後はもう少し考えて分割統治を進めたりうまくいっていないところを解消したりしていきたい。

  • コールバックを呼ぶ代わりにUIイベントのオブジェクトをイベントバス(=EventDispatcher)に流して目的別に振り分け(=Actions)、ビジネスモデルの関数を引き当てる(=ViewStates)というざっくりとした理解
    • 関心の分離の仕方は何となく好き
    • 一つのイベントを受け取って一つの状態(個々のViewの状態の集合)に遷移させるという感じのものを目指したが、Databindingと一緒に使ってると結局最後にまたばらばらにするから一つの状態にまとめる処理いらなくない?と思ってやめた
    • 画面遷移はイベントなので保存しない(RxJavaのObservable)、状態はデータなので保存する(LiveData)という考え方で組み立てている
      • 中断関数を呼び出すのが面倒(中断関数にせずRxでスレッドを切り替えるのがスマートなんだろうけど@MainThreadとかの世界には何となく戻りたくない)
    • ActionsもViewStatesもモジュールごとに分割できそう。現時点ではアプリルートのモジュールに全部書いている
  • androidx.navigationを使った画面遷移をどうすればきれいに収められるか分からなかったので残念ながらフローが分岐している
    • 画面遷移はNavHostFragmentの状態を遷移させることなので当初は自前で状態を管理しようとしていたが、NavControllerはバックスタックなども管理しているのでゆくゆくはそれも管理しなければならなくなって結局はNavControllerのようなものを作ることになるのでは…?と考えて全部androidx.navigationに寄せることにした
    • 最初はViewStatesに入れれば一方向のフローになるかなと思ってやってみたんだけどkaptがメッセージ無しの謎のエラーを吐いてこれを解決できなかったので断念した
  • テストについて:Repositoryはモックにしているが、それ以外は極力現物を使うようにしたらテストを準備するための処理が重複しまくってしまい面倒なことになってしまった。きれいにまとめる何かが必要

追記:akihito104.hatenablog.com