Flux + Jetpack Compose Todoアプリを作って学習
はじめに
今日の京都は曇っているし、湿気がすごくて頭が痛い。
ということで、以下が学習を始めたきっかけです!
- アーキテクチャの種類がMVVM以外は知らないから幅を広げたい
- 最近の業務はSwiftが多いのでKotlin触りたかった
Fluxアーキテクチャとは
FluxはFacebookが提唱する、アーキテクチャのパターンです。
データの流れが一方通行になっているのが特徴で、View、Action、Dispatcher、Storeで役割が分けられています。
lgvalle/android-flux-todo-appより
図を少しだけいじっています。
lgvalle/android-flux-todo-app
大体の構成は、以下のリポジトリを参考に勉強しました。
ありがとうございます。
seyamasan/flux-compose-todo-app
自分のリポジトリ。
このアーキテクチャのミソ
イベントの出力/受け取りがミソになってくると思うのでStateFlowを使って実現しました。
調べていく中でSharedFlowやRxJavaがありましたが、非同期処理がないsuspendする必要性がなかったのと、UIとの相性がいいらしいので今回はStateFlowを採用。
それに付随してComposeとも相性が良さそうなので、CompseでViewを書いた。
View -> Action
Viewの各イベントに応じてActionsCreatorにてActionを作成しています。
イベントによってはコールバック関数でViewから値受け取り、ActionsCreatorに渡しています。
MainScreen(
onAddClick = { actionsCreator.create(data = it) },
onDestroyClick = { actionsCreator.destroy(id = it) },
onUndoDestroyClick = { actionsCreator.undoDestroy() },
onCheckedChange = { actionsCreator.toggleComplete(it) },
onMainCheckedChange = { actionsCreator.toggleCompleteAll() },
onClearCompletedClick = { actionsCreator.destroyCompleted() },
todoStore = todoStore
)
Activity内でDIしていますが、今回はアーキテクチャの勉強に視点を置いているのでDIライブラリは使っていません。
private fun initDependencies() {
dispatcher = Dispatcher.get()
actionsCreator = ActionsCreator.get(dispatcher)
todoStore = TodoStore.get(dispatcher)
}
Action -> Dispatcher
ActionsCreatorによって作成されたActionを、Storeに渡すためにDispatcherにActionを送ります。
fun create(data: Pair<String, Boolean>) {
dispatcher.dispatch(
createAction(
TodoActionType.TODO_CREATE,
hashMapOf(TodoActionKeys.KEY_TEXT to data)
)
)
}
private fun createAction(type: TodoActionType, data: HashMap<String, Any>?): Action {
val actionBuilder = Action.type(type)
data?.let {
it.forEach { (key, value) ->
actionBuilder.setData(key, value)
}
}
return actionBuilder.build()
}
こんな感じでcreateAction(type: TodoActionType, data: HashMap<String, Any>?): Action
でActionを作成してDispatcherに渡しています。
createAction関数を、dispatch関数の引数にそのまま書いていますが、見づらいと思うので今度修正する予定。
参考にしたリポジトリは、createAction関数内の処理がdispatch関数内に書かれていたが、感覚的に分かりやすいようにActionsCreatorクラスに関数として置き換えました。
アクションのタイプやデータの形式について
アクションのタイプ
アクションのタイプは、とりあえず分かりやすくタイプが分かったら良いと考えたのでTodoActionTypeというenumクラスを作りました。
TodoActionTypeの種類は以下の通りです。
Type | Description |
---|---|
TODO_CREATE |
Create todo |
TODO_DESTROY |
Destroy todo |
TODO_COMPLETE |
Make todo complete |
TODO_UNCOMPLETE |
Uncompleted todo |
TODO_TOGGLE_COMPLETE_ALL |
Complete all todo |
TODO_DESTROY_COMPLETED |
Toggle all todo completion states |
TODO_UNDO_DESTROY |
Undo the discarded todo |
データ
データはHashMap<String, Any>またはnullです。
本当はKeyもenumにしたかったが、警告が出ていたので一旦そのままにした。
String(Key) | Any(String or Long) |
---|---|
key-text |
Text(String) |
key-id |
Id(Long) |
Dispatcher -> Store
Dispatcherは、ActionとStoreのイベントを出力するEmitterの役割を果たしています。
それらのイベントをSubscriberであるStoreに渡しています。
private val _actionFlow = MutableStateFlow<Action?>(null)
val actionFlow: StateFlow<Action?> = _actionFlow.asStateFlow()
private val _storeChangeFlow = MutableStateFlow<StoreChangeEvent?>(null)
val storeChangeFlow: StateFlow<StoreChangeEvent?> = _storeChangeFlow.asStateFlow()
fun dispatch(action: Action) {
post(action)
}
fun emitChange(changeEvent: StoreChangeEvent) {
_storeChangeFlow.value = changeEvent
}
private fun post(action: Action) {
_actionFlow.value = action
}
dispatch(action: Action)
はActionから、emitChange(changeEvent: StoreChangeEvent)
はStoreから呼ばれます。
感覚としては、dispatchはデータ出力していて、emitChangeはStoreで管理をしているデータに更新があったときにイベントを出力している感じです。
Store -> View
Dispatcherから受け取ったActionを元に、Storeで管理しているデータを更新してViewに対して更新イベントを出力しています。
更新イベントを受け取ったViewは、Storeのget関数を呼び出して更新後のデータを受け取りViewに反映します。
init {
dispatcher.actionFlow.onEach { action ->
action?.let {
onAction(it)
}
}.launchIn(CoroutineScope(Dispatchers.Main))
dispatcher.storeChangeFlow.onEach { storeChangeEvent ->
_storeChangeFlow.value = storeChangeEvent
}.launchIn(CoroutineScope(Dispatchers.Main))
}
このようにinit内でイベントを受け取った時の処理を書いています。
Actionの出力を受け取ればデータの更新と、DispatcherにStoreの状態が変わったことを出力させる。
Storeの状態が変わった出力を受け取れば、Viewに対してイベントを出力する。
LaunchedEffect(Unit) {
todoStore.storeChangeFlow.collect {
itemList = todoStore.getTodos().map { it.copy() }
}
}
todoStore.storeChangeFlow.collect
でStoreの出力を受け取り、get関数で更新されたデータを取ってきてViewを更新させています。
勉強してみて感じたこと
- イベントの出力/受け取りの処理が勉強になった
- ViewからのイベントがActionと定義付けているので感覚的に分かりやすい
- どのアーキテクチャもそうだと思うが、役割が分担されていてテストが書きやすかった
- やっぱりKotlin面白い
反省点
-
調べが不十分
早く動いているのがみたい癖がついていて、後から結構修正した。 -
StateFlowの特徴を生かせていない気がする
「初期値を持つ」という特徴があるが、nullを初期値にしているなど。
改善すること
- 早く見たい気持ちを抑える
- 勉強の内容を理解してから、使う技術を先に決める癖をつける
- 時間があればStateFlowを使っている箇所を見直す
終わりに
やっぱりAndroidアプリ開発面白いですね。
Unitテストもついでに勉強したので、なんやかんや2.5週間使いました。
分かること増やして勉強の速度を上げいていきたいです。
今回はありませんが、非同期が必要な処理も追加して初めて良さがわかる気がする。
あとUIテストと自動化も勉強したい。やりたいこと山積み。
参考にした資料など
Discussion