Skip to content

Implementation of flux application architecture on top of Kotlin Coroutines for multiplatform projects.

License

Notifications You must be signed in to change notification settings

faogustavo/fluks

Repository files navigation

Fluks

Bintray License GitHub issues

GitHub top language GitHub top language GitHub top language

Implementation of flux application architecture on top of Kotlin Coroutines for multiplatform projects.

// Define your state
data class State(
    val count: Int
) : Fluks.State

// Define your actions
sealed class Action : Fluks.Action {
    object Inc : Action()
    object Dec : Action()
    class Mult(val multiplier: Int) : Action()
    class Div(val divider: Int) : Action()
}

// Create your store
private val store: Fluks.Store<State> = store(
    initialValue = State(0),
    reducer = reducer { state, action ->
        when(action) {
            is Action.Inc -> state.copy(
                 count = state.count + 1
            )
            is Action.Dec -> state.copy(
                count = state.count - 1
            )
            is Action.Mult -> state.copy(
                count = state.count * action.multiplier
            )
            is Action.Div -> state.copy(
                count = state.count / action.divider
            )
            else -> state
        }
    },
)

// Dispatch your actions
store.dispatch(Action.Inc)
store.dispatch(Action.Dec)
store.dispatch(Action.Mult(2))
store.dispatch(Action.Div(2))

// Use the state
val currentState = store.value
store.valueFlow
    .onEach { state -> /* do something */ }
    .launchIn(scope) 

Installation

Add this implementation to you gradle file:

implementation "dev.valvassori.fluks:core:$fluks_version"

Usage

1. Create your state class inheriting from Fluks.State ;

data class State(
    val count: Int
) : Fluks.State

2. Create your actions. They have to inherit from Fluks.Action;

sealed class Action : Fluks.Action {
    object Inc : Action()
    object Dec : Action()
    class Mult(val multiplier: Int) : Action()
    class Div(val divider: Int) : Action()
}

3. Create your store inheriting from Fluks.Store and implement the abstract methods;

In this step, you can opt for two variants.

Inherit from AbstractStore;

private class Store : AbstractStore<State>() {
    override val initialValue: State
        get() = State(count = 0)

    override fun reduce(currentState: State, action: Fluks.Action): State =
        when (action) {
            is Action.Inc -> currentState.copy(
                count = currentState.count + 1
            )
            is Action.Dec -> currentState.copy(
                count = currentState.count - 1
            )
            is Action.Mult -> currentState.copy(
                count = currentState.count * action.multiplier
            )
            is Action.Div -> currentState.copy(
                count = currentState.count / action.divider
            )
            else -> currentState
        }
}

val store = Store()

or use the store helper function

val store: Fluks.Store<State> = store(
    initialValue = State(0),
    reducer = reducer { state, action ->
        when (action) {
            is Action.Inc -> state.copy(
                count = state.count + 1
            )
            is Action.Dec -> state.copy(
                count = state.count - 1
            )
            is Action.Mult -> state.copy(
                count = state.count * action.multiplier
            )
            is Action.Div -> state.copy(
                count = state.count / action.divider
            )
            else -> state
        }
    },
)

As you can see, in both of them, you must provide an initialValue and a reducer.

4. (Optional) Create your reducer

When you are using the store helper function, you can create reducer apart from the function call to improve readability and make it easier to test.

// You can also use the `Reducer` fun interface with the capital 'R'.
val storeReducer = reducer { currentState, action -> 
    when(action) {
        is Action.Inc -> state.copy(
             count = currentState.count + 1
        )
        is Action.Dec -> currentState.copy(
            count = currentState.count - 1
        )
        is Action.Mult -> currentState.copy(
            count = currentState.count * action.multiplier
        )
        is Action.Div -> currentState.copy(
            count = currentState.count / action.divider
        )
        else -> state
    }
}

5. Dispatch your actions to the store using the .dispatch(Fluks.Action) method from the store;

After having your store instance, you can dispatch your actions.

store.dispatch(Action.Inc)
store.dispatch(Action.Dec)
store.dispatch(Action.Mult(2))
store.dispatch(Action.Div(2))

6. (Optional) Adding middlewares

When required, you can add middlewares to help you update some dispatched action. The middlewares are executed in a chain, and the last node is the reducer.

To add a new middleware, create a new one using the Middleware fun interface. Then, implement the lambda with the three required parameters and return the updated state:

  • Store: The store that dispatched the action
  • Action: The action that has been dispatched
  • Next: The next node from the chain
val stateLogMiddleware = Middleware<State> { store, action, next ->
    val messages = mutableListOf(
        "[Old State]: ${store.value.count}",
        "[Action]: ${action::class.simpleName}",
    )

    val updatedState = next(action)

    messages.add("[New State]: ${updatedState.count}")
    messages.forEach { logger.log(it) }

    updatedState
}

After having an instance of your middleware, apply it to the store that you need.

// For one middleware only
store.applyMiddleware(stateLogMiddleware)

// For multiple middlewares
store.applyMiddleware(listOf(stateLogMiddleware))

If you already have a created chain of middlewares, you can just add a new one to it by calling addMiddleware(middleware).

store.addMiddleware(stateLogMiddleware)

Be careful with the applyMiddleware function if you already declared your middlewares. Each time you call this function, you create a new chain and overwrites the previous one. If you just want to add a new node, use the addMiddleware function.

Global Dispatcher

In some scenarios, you will need to dispatch an action to all of your stores (like a logout to clear the user content). If this is the case, we have a global function called dispatch(Fluks.Action) that receives and action and calls all your stores.

object Logout : Fluks.Action

val accountStore = store(emptyAccountState, accountReducer)
val ordersStore = store(emptyOrdersState, ordersReducer)

dispatch(Logout)

assertFalse(accountStore.value.isUserLoggedIn)
assertTrue(ordersStore.value.orders.isEmpty())

Combined stores

If you have more than one store, and you need to combine them to generate a new state, you can use the AbstractCombinedStore. Using it, you need to provide the stores that you depends on, and implement the combine function.

data class State0(val count0: Int) : Fluks.State
data class State1(val count1: Int) : Fluks.State
data class StateOut(val multiplication: Int) : Fluks.State

val store0 = store(State0(count0 = 1), reducer0)
val store1 = store(State1(count1 = 1), reducer1)

val combinedStores = combineStores(
    initialValue = StateOut(multiplication = 1),
    store0 = store0,
    store1 = store1,
    baseContext = Dispatchers.Main
) { s0, s1 -> StateOut(multiplication = s0.count0 * s1.count1) }