🐤

Flux + Jetpack Compose Todoアプリを作って学習

に公開

はじめに

今日の京都は曇っているし、湿気がすごくて頭が痛い。
ということで、以下が学習を始めたきっかけです!

  • アーキテクチャの種類がMVVM以外は知らないから幅を広げたい
  • 最近の業務はSwiftが多いのでKotlin触りたかった

Fluxアーキテクチャとは

FluxはFacebookが提唱する、アーキテクチャのパターンです。
データの流れが一方通行になっているのが特徴で、View、Action、Dispatcher、Storeで役割が分けられています。

lgvalle/android-flux-todo-appより
図を少しだけいじっています。

lgvalle/android-flux-todo-app

大体の構成は、以下のリポジトリを参考に勉強しました。
ありがとうございます。

https://github.com/lgvalle/android-flux-todo-app

seyamasan/flux-compose-todo-app

自分のリポジトリ。

https://github.com/seyamasan/flux-compose-todo-app

このアーキテクチャのミソ

イベントの出力/受け取りがミソになってくると思うのでStateFlowを使って実現しました。
調べていく中でSharedFlowやRxJavaがありましたが、非同期処理がないsuspendする必要性がなかったのと、UIとの相性がいいらしいので今回はStateFlowを採用。
それに付随してComposeとも相性が良さそうなので、CompseでViewを書いた。

View -> Action

Viewの各イベントに応じてActionsCreatorにてActionを作成しています。
イベントによってはコールバック関数でViewから値受け取り、ActionsCreatorに渡しています。

MainActivity.kt
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ライブラリは使っていません。

MainActivity.kt
private fun initDependencies() {
    dispatcher = Dispatcher.get()
    actionsCreator = ActionsCreator.get(dispatcher)
    todoStore = TodoStore.get(dispatcher)
}

Action -> Dispatcher

ActionsCreatorによって作成されたActionを、Storeに渡すためにDispatcherにActionを送ります。

ActionsCreator.kt
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に渡しています。

Dispatcher.kt
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に反映します。

TodoStore.kt
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に対してイベントを出力する。

MainScreen.kt
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テストと自動化も勉強したい。やりたいこと山積み。

参考にした資料など

https://github.com/lgvalle/android-flux-todo-app/tree/master
https://qiita.com/takahirom/items/0504905835ee89924ee8
https://engawapg.net/kotlin/2030/stateflow-vs-sharedflow/
https://gihyo.jp/book/2024/978-4-297-14488-3

Discussion