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)
Add this implementation to you gradle file:
implementation "dev.valvassori.fluks:core:$fluks_version"
data class State(
val count: Int
) : Fluks.State
sealed class Action : Fluks.Action {
object Inc : Action()
object Dec : Action()
class Mult(val multiplier: Int) : Action()
class Div(val divider: Int) : Action()
}
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.
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
}
}
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))
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.
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())
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) }