Skip to content

controller

Florian Schuster edited this page May 30, 2020 · 32 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, Mutation, State> {
    fun dispatch(action: Action)
    val currentState: State
    val state: Flow<State>
}

builder

a Controller is built via extension functions on a CoroutineScope

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

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

    // we start with the initial state
    initialState = State(value = 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(value = 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.

controller lifetime

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

the state Flow itself is started (meaning accepting actions, mutating and reducing), depending on the CoroutineStart parameter available in the builders.kt. the default selection is CoroutineStart.LAZY, which starts the state once Controller.state, Controller.currentState 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