Mirrativ Androidエンジニアのmorizoooです。今回はAndroidアプリをFluxにリアーキテクチャした話をします。
背景
Mirrativは2015年春頃に開発が始まり、もうすぐリリースから6周年を迎えようとしています。以前はアーキテクチャについてのルールが決まっておらず、個々人が思うがままコードを書いており、開発しているメンバーでさえ処理の流れが追えなくなっているような状況でした。そこで、まずは既存のコードの改善を行いました。詳しくは以下の記事をご覧ください。 tech.mirrativ.stream
既存コードを改善した後に、開発の指針としてFluxアーキテクチャを選定しました。
Fluxを選定した理由
- 状態がどこで更新されているのか分からなくなるという一番の課題が、Flux の導入によって解消できそうだった
- iOSはFluxで実装していくという話が出ており知見をシェアできそうだった
Fluxとは
Facebookが提唱しているデータを単一方向で取り扱うアーキテクチャです。 詳細については以下をご覧ください。 facebook.github.io
Mirrativ内の実装
Androidにおける実装として、以下の資料を参考に実装を行いました。
speakerdeck.com
Mirrativ内でのFluxのフローとしては下記の図の流れで行っています。
具体例としてLive情報をAPIから取得して表示を行うサンプルを紹介します
ActionCreator
- ActionEventをDispatcherを経由して状態の変更をStoreに通知する
- function名はシステムに対する要求を書く
- ユーザーのイベントをどうハンドルするかはViewに任せる
- animationさせる、logを飛ばす、別のactionを呼ぶようなことはしない
class LiveActionCreator( private val dispatcher: Dispatcher, private val mirrativRequest: MirrativRequest ) : CoroutineScope { override val coroutineContext: CoroutineContext = Dispatchers.IO + Job() fun fetchLive() { launch { try { val response = mirrativRequest.getLive() dispatcher.dispatch(LiveActionEvent.FetchLiveSucceeded(response)) } catch (error: MirrativError) { dispatcher.dispatch(LiveActionEvent.FetchLiveFailed(error)) } } } }
ActionEvent
- アプリケーションへの変更内容を定義したデータ
- EventBusを使ってデータを送信しているのでActionEventとしている
sealed class LiveActionEvent { // システムの要求結果をEventとして流す // アクションのEvent名は内容を見なくても、アプリケーションで何が起こったのかを十分に理解できるようにする // システムの要求結果なのでEvent名は過去形にする。 data class FetchLiveSucceeded(val live: LiveResponse) : LiveActionEvent() data class FetchLiveFailed(val error: MirrativError) : LiveActionEvent() }
Dispather
- ActionCreatorから渡されたActionEventをEventBusを用いてStoreに通知する
class Dispatcher { private val dispatcherBus: EventBus = EventBus.getDefault() fun dispatch(event: Any) { dispatcherBus.post(event) } fun register(observer: Any) { if (!dispatcherBus.isRegistered(observer)) { dispatcherBus.register(observer) } else { Logger.error("Subscriber $observer already registered to event ") } } fun unregister(observer: Any) { if (dispatcherBus.isRegistered(observer)) { dispatcherBus.unregister(observer) } else { Logger.error("Subscriber to unregister was not registered before: $observer") } } }
Store
- 現在のStoreの状態とDispatcherから飛んできたActionEventの情報からBindModelの作成・更新を行います
- APIレスポンスをBindModelへ変換する処理はonメソッドの中で行う
- Action/Storeは対になっている必要はない、どんなEventをobserveしても良い
- Viewの状態ではなく、成功/失敗のようなEventを返したい場合はSharedFlowを使う
- Storeのデータを更新できるのはStore内のonメソッドのみ
class LiveStore( private val dispatcher: Dispatcher ) : ViewModel(), CoroutineScope by MainScope() { init { dispatcher.register(this) } override fun onCleared() { cancel() dispatcher.unregister(this) super.onCleared() } private val mutableLiveBindModel = MutableLiveData<LiveBindModel>() val liveBindModel: LiveData<LiveBindModel> = mutableLiveBindModel private val mutableErrorFlow = MutableSharedFlow<MirrativError>() val errorFlow: SharedFlow<MirrativError> = mutableErrorFlow @Subscribe(threadMode = ThreadMode.MAIN) fun on(event: LiveActionEvent.FetchLiveSucceeded) { LiveBindModel.convert(event.live) } @Subscribe(threadMode = ThreadMode.MAIN) fun on(event: LiveActionEvent.FetchLiveFailed) { launch { mutableErrorFlow.emit(event.error) } } }
BindModel
- Viewを描画するためのプロパティを持つ
- プリミティブしか持たない
class LiveBindModel(
val liveId: String,
val liveTitle: String,
val userName: String,
val profileImageUrl: String,
val isVisibleProfileImage: Boolean,
) {
companion object {
fun convert(live: Live) = LiveBindModel(
live.userId,
live.liveTitle,
live.user.userName,
live.user.imageUrl,
live.user.imageUrl.isNotEmpty(),
)
}
}
View
- StoreのimmutableなLiveDataを監視し、BindModelを用いて画面を変更する
- ユーザーの操作をActionCreatorを用いて実行する
- API失敗時にトーストを出すような複数回実行してはいけないイベント処理はSharedFlow経由で行う
class LiveActivity : AppCompatActivity(), CoroutineScope by MainScope() { // StoreとActionCreatorはKoinを使ってDIしています private val liveStore: LiveStore by viewModel() private val liveActionCreator: LiveActionCreator by inject() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val binding = LiveActivityBinding.inflate(layoutInflater) setContentView(binding.root) liveStore.liveBindModel.observe(this, Observer { binding.titleTextView.text = it.liveTitle binding.userNameTextView.text = it.userName ... } launch { liveStore.errorFlow.collect { Toast.makeText(this@LiveActivity, it.error,Toast.LENGTH_SHORT).show() } } liveActionCreator.fetchLive() } override fun onDestroy() { super.onDestroy() cancel() } }
まとめ
Mirrativの配信や視聴画面ではサーバーとのHTTP通信だけではなく、WebSocketやWebRTCでの通信やUnityからのCallBackなど状態の更新が非同期で様々な場所で行われています。複雑な処理になればなるほど、Storeに最新の状態が反映されていることで処理の流れが追いやすくなるというメリットを享受できます。
Fluxへのリアーキテクチャは9割程終わっており、普段触るコードではFluxに書き換わっています。現在はViewの開発速度を向上させるためにJetpackComposeの導入にトライしています。
We are hiring!
ミラティブでは一緒にアプリを作ってくれる Android エンジニアを募集中です!気軽にご連絡ください! www.mirrativ.co.jp
開発者向けのイベントも積極開催中です!ぜひご参加ください! radiotalk.connpass.com