Giter Site home page Giter Site logo

kostore's Introduction

KoStore

A functional implementation of Redux for Kotlin.

Example

Imagine creating an app where the user should login. When the user is not logged in, he should use his username and password to login. The credentials are send to a server and it will return us a token or an error.

To work with Redux a developer has to make three steps:

  1. Create the reducer
  2. Create the middleware
  3. Add these to the store

1. Create the reducer

There are three states involved:

sealed class LoginState {
    data class NotLoggedIn(val reason: String? = null) : LoginState()
    object LoggingIn : LoginState()
    data class LoggedIn(val token: String) : LoginState()
}

Next step is to inventorise how the state can transition to another. The object necessary to do so, is called the action.

sealed class LoginAction {
    data class Login(val name: String, val pass: String) : LoginAction()
    data class Success(val token: String) : LoginAction()
    data class Failure(val error: String) : LoginAction()
    object Logout : LoginAction()
}

To transition from one state to another, we will create a Reducer<T>. This is a typealias, which is a readable name for a function that has the signature: (State,Action) -> State.

val userReducer: Reducer<LoginState> = { state: LoginState, action: Any ->
    when (action) {
        is LoginAction.Login -> LoginState.LoggingIn
        is LoginAction.Success -> LoginState.LoggedIn(action.token)
        is LoginAction.Failure -> LoginState.NotLoggedIn(action.error)
        is LoginAction.Logout -> LoginState.NotLoggedIn()
        else -> state
    }
}

This reducer is not entirely correct, since the state can transition from LoggedOut to LoggedIn without first going to LoggingIn. To create for ourselves an overview of all possible transitions, one can make a state table:

current action next
NotLoggedIn Login LoggingIn
LoggingIn Success LoggedIn
LoggingIn Failure NotLoggedIn
LoggedIn Logout NotLoggedIn

With KoStore it is easy to convert such table to code with the use of a TableReducer.

val loginTableReducer: Reducer<LoginState> = TableReducer<LoginState> {

    state<LoginState.NotLoggedIn>()
            .withAction<LoginAction.Login>()
            .creates { LoginState.LoggingIn }

    state<LoginState.LoggingIn>()
            .withAction<LoginAction.Success>()
            .creates { action -> LoginState.LoggedIn(action.token) }

    state<LoginState.LoggingIn>()
            .withAction<LoginAction.Failure>()
            .creates { action -> LoginState.NotLoggedIn(action.error) }

    state<LoginState.LoggedIn>()
            .withAction<LoginAction.Logout>()
            .creates { LoginState.NotLoggedIn() }

}

2. Create the middleware

Whenever the login action is received, it has to do a (asynchronous) network operation. During the operation the state is 'LoggingIn'. These side effects are done within the middleware function. The example below checks whether the action is LoginAction.Login and when that's true it will emit the action and either failure or success.

NOTE: In the example threading is not handled, but it is good practices to call next() always on the same thread on which the Store is used.

typealias Callback = (error: Exception?, token: String?) -> Unit
typealias NetworkOperation = (name: String, pass: String, callback: Callback) -> Unit

fun networkMiddleware(networkOperation: NetworkOperation): Middleware<LoginState> =
        { getState: () -> LoginState, dispatch: (Any) -> Unit, action: Any, next: (Any) -> Unit ->

            if (action is LoginAction.Login) {
                next(action)
                networkOperation(action.name, action.pass) { error, token ->
                    if (error != null) {
                        next(LoginAction.Failure(error.message!!))
                    } else {
                        next(LoginAction.Success(token!!))
                    }
                }
            } else {
                next(action)
            }

        }

Calling next basically passes it parameter to the reducer. Whenever state is LoggedIn or NotLoggedIn it needs to be persisted.

fun persistMiddleware(persist: (LoginState) -> Unit): Middleware<LoginState> =
        afterNext { getState: () -> LoginState, dispatch: (Any) -> Unit, action: Any, next: (Any) -> Unit ->
            val state = getState()

            if (state !== LoginState.LoggingIn)
                persist(state)

        }

These two middlewares represent the middleware necessary to change the LoginState. To make these easier to (re)use, we can bundle them into one middleware:

// the parameters are stubbed for this example
fun loginMiddleware(
        networkOperation: NetworkOperation = { _, _, _ -> },
        persist: (LoginState) -> Unit = {}
): Middleware<LoginState> =
        arrayOf(
                networkMiddleware(networkOperation),
                persistMiddleware(persist)
        ).reduce(::combine)

3. Add these to the store

The goal of using Redix is having an object to which you can send actions to and receive state changes through an observer. The object that facilitates this, is called the 'Store'.

Usually a store is a composition of multiple (sub-)states. Therefore the Store gives access to a initialization DSL in which you can install multiple reducers and middlewares.

data class AppState(val state: LoginState = LoginState.NotLoggedIn())

val store: Store<AppState> = Store(AppState()) {

    // compose facilitates the working between a state (AppState) and it's substate (LoginState)
    compose({ it.state }, { copy(state = it) }) {
        addReducer(loginReducer)
        addMiddleware(loginMiddleware())
    }

}

For convience there are also compose functions for working with Collection, List and Map.

Calling store.dispatch() will emit the action to the middleware. The middleware will pass it results to the reducer by calling next() and the app listens to the state changes through store.addObserver().

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.