1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

株式会社ゆめみAdvent Calendar 2024

Day 11

Comopose のコンポーネントで Tart を利用する

Last updated at Posted at 2024-12-13

Tart という Kotlin Multiplatform 向けの Flux ライクな状態管理フレームワークを作っています。(いちおう現時点で rc-02 として Maven Central にも publish してします。)

これは一応、ネィティブアプリ(Android /iOS)の「画面」の状態管理用に作っていたのですが、ふと、別に画面全体でなくても、Compose のコンポーネント単位で利用してもよいな:thinking: と思いついたので試してみました。

今回は、次のような簡単なカウンターのコンポーネントを作ってみます。

スクリーンショット 2024-12-13 17.07.08.png


まず、カウンター用の StateActonEvent を用意します。

data class CounterState(val count: Int) : State

sealed interface CounterAction : Action {
    data object Increment : CounterAction
    data object Decrement : CounterAction
}

sealed interface CounterEvent : Event // empty

次に、Store を用意します。

class CounterStore(
    coroutineContext: CoroutineContext,
) : Store.Base<CounterState, CounterAction, CounterEvent>(
    initialState = CounterState(count = 0),
    coroutineContext = coroutineContext,
) {
    override suspend fun onDispatch(state: CounterState, action: CounterAction): CounterState = when (action) {
        is CounterAction.Increment -> {
            state.copy(count = state.count + 1)
        }

        is CounterAction.Decrement -> {
            if (0 < state.count) {
                state.copy(count = state.count - 1)
            } else {
                state // do not change State
            }
        }
    }
}

これでロジック的なものは定義できたので、最後に目的である、Compose のコンポーネントを作ります。

@Composable
fun Counter() {
    val scope = rememberCoroutineScope()
    val store = remember {
        CounterStore(
            coroutineContext = scope.coroutineContext,
        )
    }
    val viewStore = rememberViewStore(store)

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Text(
            text = viewStore.state.count.toString(),
        )
        Row {
            Button(
                onClick = { viewStore.dispatch(CounterAction.Increment) },
            ) {
                Text(text = "Increment")
            }
            Spacer(
                modifier = Modifier.width(8.dp),
            )
            Button(
                onClick = { viewStore.dispatch(CounterAction.Decrement) },
            ) {
                Text(text = "Decrement")
            }
        }
    }
}

アプリを起動すると、きちんと動作していることが分かります :thumbsup:

Repository と連携してみる

さらに、このコンポーネントに Repositoy を連携して、データの取得や保存ができるのではないか?と思ったので、やってみます。

コンポーネントで直接、Repository を取り扱うのは微妙な気がするので、Store の factory クラスを用意して、ここに Repository を持たせます。ここでは省略しますが、この factory のインスタンスは、Dagger 等で Activity に inject します。

class CounterStoreFactory(
    private val counterRepository: CounterRepository,
) {
    fun create(coroutineContext: CoroutineContext): CounterStore {
        return CounterStore(
            counterRepository = counterRepository,
            coroutineContext = coroutineContext,
        )
    }
}

Store を、Repository を通してデータ取得、保存するように書き換えます。

class CounterStore(
    private val counterRepository: CounterRepository,
    coroutineContext: CoroutineContext,
) : Store.Base<CounterState, CounterAction, CounterEvent>(
    initialState = CounterState(count = -1), // 初期値はいったん -1 にしておく
    coroutineContext = coroutineContext,
) {
    override suspend fun onEnter(state: CounterState): CounterState {
        val count = counterRepository.get() // データをロード
        return state.copy(count = count)
    }

    override suspend fun onDispatch(state: CounterState, action: CounterAction): CounterState = when (action) {
        is CounterAction.Increment -> {
            val count = state.count + 1
            state.copy(count = count).apply {
                counterRepository.set(count) // データをセーブ
            }
        }

        is CounterAction.Decrement -> {
            if (0 < state.count) {
                val count = state.count - 1
                state.copy(count = count).apply {
                    counterRepository.set(count) // データをセーブ
                }
            } else {
                state // do not change State
            }
        }
    }
}

コンポーネントは、factory を受け取って、それを Store の作成に利用するようにします。

@Composable
fun Counter(counterStoreFactory: CounterStoreFactory) {
    val scope = rememberCoroutineScope()
    val store = remember {
        counterStoreFactory.create(scope.coroutineContext)
    }
    val viewStore = rememberViewStore(store)

    // ... あとは同じ
}

これでデータの永続化ができました :thumbsup:

おわりに

最初に意図したライブラリの使い方ではないですが、Compose のコンポーネントと合わせて、本ライブラリの StoreStateActionEvent のクラス群、あと Repository や UseCase を連携する場合は Store の factory クラス、を一式で用意しておけば、再利用可能なコンポーネントが作りやすいのかなあと思ったりしました。

[追記] 検討項目

  • 画面回転時や Activity 破棄時の State の保存
    • rememberSaveable を使う?
  • factoriy クラスそコンポーネントに引き回すの面倒問題
    • CompositionLocal を使う?
  • コンポーネントの preview への対応
    @Composable
    fun Counter(counterStoreFactory: CounterStoreFactory) {
        val scope = rememberCoroutineScope()
        val store = remember {
            counterStoreFactory.create(scope.coroutineContext)
        }
        val viewStore = rememberViewStore(store)
    
        SubComponent(viewStore) // こっちに内容を移してpreviewの対象とする?
    }
    
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?