前回: 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
を受けとってそれぞれのイベントに変換するようなものなど)はまだ従来のままなので、なるべくシンプルな記述ですむ方法を考えたいと思っている。