Skip to content

controller

Florian Schuster edited this page Oct 24, 2021 · 31 revisions

A Controller is an ui-independent class that controls the state of a view. The role of a Controller is to separate business-logic from view-logic. A Controller has no dependency to the view, so it can easily be unit tested.

flow

interface Controller<Action, State> {
    fun dispatch(action: Action)
    val state: StateFlow<State>
}

builder

a regular Controller is built via extension function on a CoroutineScope

// action triggered by view
sealed interface Action {
    data class SetValue(val value: Int) : Action
}
 
// mutation that is used to alter the state
private sealed interface Mutation {
    data class SetMutatedValue(val mutatedValue: Int) : Mutation
}
 
// immutable state
data class State(
    val counterValue: Int
)

// Controller is created in a CoroutineScope
val valueController = someCoroutineScope.createController<Action, Mutation, State>(

    // we start with the initial state
    initialState = State(counterValue = 0),

    // every action is transformed into [0..n] mutations
    mutator = { action ->
        when (action) {
            is Action.SetValue -> flow {
                delay(5000) // some asynchronous action
                val mutatedValue = action.value + 1
                emit(Mutation.SetMutatedValue(mutatedValue))
            }
        }
    },

    // every mutation is used to reduce the previous state to a 
    // new state that is then published to the view
    reducer = { mutation, previousState ->
        when (mutation) {
            is Mutation.SetMutatedValue -> previousState.copy(counterValue = mutation.mutatedValue)
        }
    }
)

since initialState, mutator, reducer and the different transformation functions can be provided via the builder of the Controller, a high degree of composition is possible.

lifetime

the Controller lives as long as the CoroutineScope is active that it is built in.

the internal state machine is started (meaning accepting actions, mutating and reducing), depending on the ControllerStart parameter available in the builders. the default selection is ControllerStart.Lazy, which starts the state machine once Controller.state or Controller.dispatch(...) are accessed.

errors

when any Throwable's are thrown inside Controller.mutator or Controller.reducer they re-thrown as wrapped RuntimeException's.

Clone this wiki locally