fresh digitable

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

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

つづきです akihito104.hatenablog.com

  • 今:Eventを流してModelを更新し、その結果ViewStateが更新される
  • 課題:Modelを更新したときに出てくる例外をどうするか

例えばこんな感じのハッピーパスしか考えてないやつ(スレッドとか中断関数とかそういうことは端折っています):

val state: LiveData<State> = updateEventFlowable
    .map { event -> model.update(event.text) }
    .toLiveData()

model.update()で例外が起きるとLiveDataでは例外を処理できない的なメッセージが出てクラッシュする。こういうケースでどうするか考えていく。

  • 想定される例外(IOExceptionとかAPI limit exceeded的なやつ)が起きたときはリカバリしたい
    • リカバリは今のところやり直すとかいうよりは「だめだったからあとでやり直してね」ってToastとかSnackbarとかでユーザーに伝えるぐらいしかない
  • 想定外の例外が起きてリカバリできない(どうするのが正しいのか分からない)時はちゃんと大域脱出(クラッシュ)したい
    • テストの文脈(mockkとか)で出てくるような例外を握りつぶさないように
    • でも例えばFragmentスコープでは解決できなくてもActivityスコープとかApplicationスコープでなら解決できるならそのスコープでキャッチしてリカバリしてもいいと思う

Event -> (Model) -> ViewState という流れとは別に、 Event -> (Model) -> 例えばMessage のような終端も必要ということかなと考えて、

  • Eventを流してModelを更新し、その結果をResultという形で得る
  • ResultからViewStateを得る、もしくはResultから例外を得てリカバリする

のような感じで、間にもう1クッション挟んで分岐できるようにしたらいいのだろうか。だんだん巨大になっていくのでもっといい抽象化の仕方があるのではないかという気がする。結論は出ていないけどとりあえずその方向で実装して動きを見てみようと思う。

あとは、RepositoryからLiveDataを返すような関数を生やしていたけど、上のようなことを考慮するにあたってLiveDataを返すようなのはこのケースには合わないのかなと思い始めている。この辺はまだもやもやしていてうまく説明できない。でもRepositoryのコードがViewStatesにどんどん移動していくことになりそう。

追記: akihito104.hatenablog.com

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

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

追記: akihito104.hatenablog.com

*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