Skip to content

Core Concepts

Abhi Muktheeswarar edited this page Aug 2, 2021 · 15 revisions

Flywheel flowchart

Flywheel_Flowchart

State

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

Action

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.

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.

StateReserve

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())

SideEffect

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)) 
        }
    }
}

Middleware

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)
            }
        }
    }
}

Listen to state changes

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
    }
}

Accessing State

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.

Threading

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.

Clone this wiki locally