-
Notifications
You must be signed in to change notification settings - Fork 3
Core Concepts
The state holds the data of our app. For example, the state of a counter app might look like this:
data class CounterState(val counter: Int = 0) : State
To change something in the state, you need to dispatch
an Action
. Here are a few example actions:
sealed interface CounterAction : Action {
object IncrementAction : CounterAction
object DecrementAction : CounterAction
}
Actions are the inputs to the Flywheel. The state can be changed only by an action. To tie state and actions together, we write a function called a reducer
.
A Reducer just a pure function that takes action and state as arguments and returns the next state of the app.
val reducer = reducerForAction<CounterAction, CounterState> { action, state ->
with(state) {
when (action) {
is CounterAction.IncrementAction -> copy(counter = counter + 1)
is CounterAction.DecrementAction -> copy(counter = counter - 1)
}
}
}
This is the basic idea of Flywheel.
To tie all this together, we have StateReserve
. A StateReserve holds the state and the reducer function. It receives the actions and updates the state using the reducer.
val stateReserve = StateReserve(initialState = InitialState.set(CounterState()), reduce = reducer, middlewares = emptyList())
To handle async tasks, we have SideEffects. From SideEffect
we can listen to actions, state changes from StateReserve
. Through SideEffect
, For example, we can listen for a particular Action
and do something like fetching data from the network and in turn, can dispatch another Action
to the StateReserve
to update the state.
class GetItemsSideEffect(private val repository: Repository, stateReserve: StateReserve<ItemsState>) : SideEffect(stateReserve) {
init {
stateReserve.actionStates.onEach(::handle).launchIn(scope)
}
private fun handle(actionState: ActionState<Action, ItemsState>) {
when (actionState.action) {
is GetItemsAction -> {
if (actionState.state.items.isEmpty()) {
getItemsFromNetwork()
}
}
}
}
fun getItemsFromNetwork() {
scope.launch {
val items = repository.getItems()
dispatch(UpdateItemsAction(items))
}
}
}
What if you need to intercept the action before reaching the reducer or modify the action before reaching the state or just prevent the action from reaching the reducer. For this, we have Middleware
. For example, we don't want a ShowToastAction
to reach the reducer unnecessarily. To achieve that, we can have a middleware that swallows the ShowToastAction
. The concept of Middleware in Flywheel differs from Redux. In Redux, async tasks are taken care of by Middlewares. But in Flywheel, we have SideEffect
to take care of async tasks. Using Middleware(s) is optional in Flywheel.
val skipMiddleware: Middleware<State> = { _, _ ->
{ next ->
{ action ->
if (action is ShowToastAction) {
//Don't do anything
} else {
//Pass on the action to reducer
next(action)
}
}
}
}
You can listen to state changes in your View layer. The state is exposed as Flow<State>
.
class CounterScreen : AppCompatActivity() {
private val viewModel by viewModels<CounterViewModel>()
override fun onStart() {
super.onStart()
lifecycleScope.launchWhenCreated {
viewModel.states.collect(::setupViews)
}
}
private fun setupViews(state: CounterState) {
textView.text = state.counter
}
}
If you just want to retrieve the value of state once, you can use stateReserve.state()
function. If you want to retrieve the state once, guaranteeing that all the existing or previous actions in the queue are processed by the reducer, you can use the stateReserve.awaitState()
suspend function.
Since Flywheel is based on coroutines, we don't have to directly interact with threads. By default, StateReserve's reducer and the associated SideEffects runs on Dispatchers.Default
coroutine context (i.e: everything runs in the background thread(coroutine). So no need to worry about blocking the main thread.