fresh digitable

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

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

前回: akihito104.hatenablog.com の最後に、

とくにActionsはもっと定型化できるように思う。

というようなことを書いた。ここまでの実装は、

class HogeActions(
  private val dispatcher: EventDispatcher,
) : HogeEventListener {

  val selectAccount: Flow<HogeEvent.AccountSelected> = dispatcher.toActionFlow() // EventDispatcherから所望のAppEventを取り出して下流に流すFlowを作る関数

  override fun onAccountSelected(account: UserAccount) {
    dispatcher.post(HogeEvent.AccountSelected(account)
  }

  val login: Flow<HogeEvent.LoginClicked> = dispatcher.toActionFlow()

  override fun onLoginClicked() {
    dispatcher.post(HogeEvent.LoginClicked)
  }
  ...
}

のように、イベントバスにイベントを流す処理と、イベントバスを監視して所望のイベントオブジェクトが流れてきたら下流に流す処理とを別々に書いている。それぞれが対応しているということをお互いに近づけて書くことで表現しているのだが、流すイベントのクラスと受け取るイベントのクラスを取り違える危険性があるし、同じような言葉を何度も書かなければならず冗長すぎる。本家Cycle.jsのIntentクラスのようにすっきりさせたい。例えば次のようなイメージで、プロパティの方だけ書けばいいというようなもの。

val selectAccount: ??? = dispatcher.to??? { account -> 
  HogeEvent.AccountSelected(account)
}
val login: ??? = dispatcher.to???(HogeEvent.LoginClicked)

EventListenerの関数でもありFlowでもあるようないい感じのクラスと、それを作るファクトリ関数を考えたい。関数をfunとして*1定義するとこのようにはできないので、関数型のクラスを定義して置き換えてみる。例えば、

interface AppEventListener {
  fun dispatch()
}

interface AppAction<E : AppEvent> : AppEventListener, Flow<E : AppEvent>

fun <E : AppEvent> EventDispatcher.toAction(event: E): AppAction<E> {
  return object : AppAction<E>, Flow<E> by this.toActionFlow() {
    override fun dispatcher() {
      this@toAction.post(event)
    }
  }
}

のようなinterfaceを定義して、ついでにEventDispatcherからAppActionを作る便利関数を作ると、例で挙げたHogeActionsクラスは次のように書ける。

interface HogeEventListener {
-  fun onLoginClicked()
+  val login: AppEventListener
  ...
}

class HogeActions(
  private val dispatcher: EventDispatcher,
) : HogeEventListener {
  ...
-  val login: Flow<HogeEvent.LoginClicked> = dispatcher.toActionFlow()
+  override val login = dispatcher.toAction(HogeEvent.LoginClicked)

-  override fun onLoginClicked() {
-    dispatcher.post(HogeEvent.LoginClicked)
-  }
  ...
}

ちなみに、DataBindingでレイアウトリソースからEventListenerのプロパティを呼び出すと android:onClick="@{ viewModel.login.dispatch() }" のような感じになる。レイアウトリソースファイルはJavaの世界なので、AppEventListenerの中の関数をoperator fun invokeにしてもinvokeと書かなければならない。invokeでもいいような気もするけどEventDispatcherでも使っている言葉をここでも使うことにしてdispatchを選んだ。

EventListenerの関数が引数をとるような場合も同様に、と言いたいところだけどいろいろ考えだしたらよく分からなくなってしまったので、とりあえず引数を1つだけとるものを用意してみた。

interface AppEventListener1<T> {
  fun dispatch(t: T)
}

interface AppAction1<T, E : AppEvent> : AppEventListener1<T>, Flow<E : AppEvent>

fun <T, E : AppEvent> EventDispatcher.toAction(block: (T) -> E): AppAction1<T, E> {
  return object : AppAction1<T, E>, Flow<E> by this.toActionFlow() {
    override fun dispatcher(t: T) {
      val event = block(t)
      this@toAction.post(event)
    }
  }
}

このようなinterface及びファクトリ関数を使うことで、次のように書ける。

interface HogeEventListener {
-  fun onAccountSelected(account: UserAccount)
+  val selectAccount: AppEventListener1<UserAccount>
  ...
}

class HogeActions(
  private val dispatcher: EventDispatcher,
) : HogeEventListener {

-  val selectAccount: Flow<HogeEvent.AccountSelected> = dispatcher.toActionFlow()
+  override val selectAccount = dispatcher.toAction { account: UserAccount ->
+    HogeEvent.AccountSelected(account)
+  }

-  override fun onAccountSelected(account: UserAccount) {
-    dispatcher.post(HogeEvent.AccountSelected(account)
-  }
  ...
}

少しはすっきりしただろうか。それとも悪乗りがすぎるだろうか。引数を2つ以上とるものや、1つの関数から複数種類のイベントを発生させるもの(MenuItemを受けとってそれぞれのイベントに変換するようなものなど)はまだ従来のままなので、なるべくシンプルな記述ですむ方法を考えたいと思っている。

*1:という言い方が正しいかどうかわからない。Javaでいうところのメソッド