SansanでAndroidアプリケーションエンジニアをしている山口 です。リードエンジニアになって8ヶ月が過ぎました。
今回はSansan AndroidにおけるFlux移行について書こうかなと思います。
Flux移行の背景
アーキテクチャの変更に限らず新しいなにかの導入は理由があって行うものです。流行り廃りに敏感になることは重要かと思いますが、現状の開発に問題がない場合、あえて古い技術要素のまま開発を進めることは多々あります。今回Fluxへ移行する理由は以下の3つです。
- 体制の変更とそれに伴う現状のアーキテクチャにおける問題点を解決する
- データの表示というアプリの本来の目的にフォーカスする
- チームメンバーが好きになれるアーキテクチャを採用する
体制の変更とそれに伴う現状のアーキテクチャにおける問題点を解決する
前回この1年の変化を書かせていただいた中でチームメンバーの増加とMVPのレイヤードアーキテクチャについて触れました。
前回の記事の7月時点ではAndroid1チームで開発を行っていましたが現在はリードエンジニアをもう1人立て、2チームに分割されました。更に業務委託メンバーがフルタイムに、12月からは新メンバーが増え、4人チーム+3人チームの計7人の体制になります。
さて人数が増えてくるとより並列に開発が行えるようになります。そこでの問題の1つがコンフリクトによる開発スピードの低下です。 MVPのレイヤードアーキテクチャにおいて、例えば名刺を取得して表示する画面の場合、ViewロジックをBizCardPresenterに、ドメインロジックをBizCardUseCaseに書きます。複数人でこの1画面を開発すると、複数人でBizCardPresenter/UseCaseを変更することになり、コンフリクトする可能性が高くなります。
更に、1画面におけるViewロジックがPresenterに、ドメインロジックがUseCaseに集まる結果、Presenter/UseCaseが肥大化する傾向にあります。これはプログラムにおける可読性の低下や運用保守性の低下に繋がります。MVPの特性上ViewからPresenter間、PresenterからUseCase間を行き来することが多く、こちらも可読性を下げる原因になっていました。
もう1つの問題点が、状態の管理についてあまり深く定義していないため実装者によって状態の管理方法が異なることが増えてきた点です。View側で状態を持つ場合もあればPresenter側で状態を持つ場合、またはView自体に設定された値を内部から取ってきて状態を復元するなど統一性が取れなくなってきました。
データの表示というアプリの本来の目的にフォーカスする
アプリケーションによってそのアプリケーションがフォーカスすべきものは異なります。Sansanアプリは名刺をカメラで撮影して登録する機能や取引先とのコンタクトを記録する機能などがありますが、アプリにおける大きな目的は社内の人脈データを表示し、ビジネスに役立ててもらうことです。
現状のMVPのレイヤードアーキテクチャの構成はView用にデータを用意するというよりは、UseCaseから取得してきたデータからPresenterががんばって表示を切り替える構造になっています。場所によってはUseCase側でView用にデータを加工しているところもありますが、生データをそのままViewに流してView側で切り替えていたり、データは表示できていても各層でのデータの扱いがまばらでした。
今回のFlux移行ではViewが状態を管理するStoreをobserveしていればView用にフォーカスされたデータが流れてきてそれを表示するだけの構造としました。そうすることでView以外のクラス郡はViewの表示のためにデータを取得・加工しStoreに流すために動くという全体の方針が確立できました。
チームメンバーが好きになれるアーキテクチャを採用する
これは重要じゃないように見えて重要だと考えています。開発を行うのは人であり、人によって好き嫌いはあります。長く開発を続けていく中のモチベーションにも大きく関わってくると思います。もちろんそれだけを理由にアーキテクチャを変更することはありませんが、アーキテクチャ変更をひと押しする理由の1つではあると思っています。
Sansan AndroidにおけるFlux
FluxはFacebook発祥のアーキテクチャで主にWeb開発で使われているアーキテクチャです。近年ではモバイルアプリケーションにおいても採用事例が増えてきています。
Fluxは広義の意味でのMVVMと捉えています。モバイルアプリケーションはユーザのアクションやライフサイクルイベントを起点に非連続にデータの取得・変換を行い、その結果を表示します。observerパターンとしてデータを監視してViewを更新するMVVMの構造は次々にイベント後の結果をハンドリングする特性に合っています。またobserverパターンを適用することでViewへの参照をViewロジックから分離できます。Googleは公式にMVVMを推奨しており、LiveDataやViewModelなどのobserverパターンやデータストアの仕組みを公式ライブラリとして提供している点としても導入のハードルが低いです。今回StoreはLiveDataを持つViewModelとして実装しています。
abstract class Store<T>(private val dispatcher: Dispatcher) : ViewModel(), Dispatcher.ActionListener { init { @Suppress("LeakingThis") dispatcher.register(this) } val data: LiveData<T> = MutableLiveData<T>() protected fun update(value: T) { (data as MutableLiveData<T>).value = value } protected fun update(action: Action<T>) { (data as MutableLiveData<T>).value = action.payload } override fun onCleared() { super.onCleared() dispatcher.unregister(this) } }
FluxはMVVMの特性の上に状態の管理はStore、生成はActionCreator、通知処理はDispatcher、と処理を分離する構造になっています。一方でクラスは増えますが、それぞれのクラスが単一責務で処理に集中できるのでどこで状態を作ってどこで管理するかを迷うことがなく、Presenter/UseCaseを分割して各クラスの肥大化を解決できます。Fluxでは単一方向のフローを実現できるため処理フローを追いやすく、実装時は一貫したフローに統一できます。 Fluxの各クラスに加えてStateクラスを加えて構築しています。各クラスの役割は以下の通りです。
- State
- データ表示・イベントハンドリングのためのドメインモデル
- Viewが直接バインドできるデータ
- Intentで次の画面に渡すデータ
- データ表示・イベントハンドリングのためのドメインモデル
- View
- UIを表示する
- Storeをobserveし、通知されたStateを元にViewにバインドする
- イベントハンドリングをする
- イベントに応じてActionCreatorを呼ぶ
- 画面遷移を行う
- UIを表示する
- Action
- Stateをペイロードとして持ち、Dispatcherに渡される
- ActionCreatorにより生成される
- Store
- Stateを1つ保持する
- Dispatcherに自身を登録し、Actionが持つStateを通知する
- Dispatcher
- 渡されたActionを登録された全Storeに通知する
- ActionCreator
- 現在の状態やAPI、DBからのデータを元に新たなStateおよびActionを作成し、Dispatcherに渡す。必要に応じて1つのActionCreatorで複数のActionを作成することもある
もう1つの利点としてActionまたはActionCreatorを共有することによって、いわゆる'いいね問題'を解決しやすくなります。例えばある画面で最新の名刺3件を表示し、もっとみるを押したら一覧を表示するとします。一覧から詳細に行って編集して戻ったときに一覧の再読み込みを行うと思いますが、共有されたActionCreatorを実行することで最新の名刺3件のほうも更新される構造を取ることができます。
おわりに
まだまだアプリケーション全体でFluxを適用できている訳ではありませんが、これからの体制や現状の問題解決のために一歩踏み出せたのは大きいと感じています。Fluxを横展開していく中でこういうときはどう処理をすればよいだろう?、こうしたいけどできないという議論は度々起きており、Flux基盤側を拡張したり自分たちなりのベストプラクティスを見つけようと日々模索中です。引き続きより良いプロダクトを効率的に開発できるように取り組んでまいります。
モバイルエンジニアを絶賛募集中です。興味のある方は話を聞きに来ていただくだけでも構わないのでお待ちしております。 hrmos.co