Giter Site home page Giter Site logo

coredux's Introduction

CoRedux

Opinionated Redux implementation using Kotlin coroutines inspired by RxRedux.

Table of content

Getting started

Gradle

All release artifacts are hosted on Jitpack:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

dependencies {
    implementation 'com.github.okatrych.coredux:core:1.0'
}

What is CoRedux

CoRedux is a predictable state container, that is using same approach as Redux with ability to have additional side effects.

Implementation is based on Kotlin coroutines and inspired by both RxRedux library and Coroutines in practice by Roman Elizarov KotlinConf2018 talk.

Basic concept

Imagine - we need to develop a calculator app that has on new instance of app start initial value (state) 0. Calculator only allows addition, deduction, multiplication and division operations. Operations describe what should happen with the calculator current state and can be represented as following input actions:

sealed class CalculatorAction {
    data class Add(val value: Int) : CalculatorAction()
    data class Deduct(val value: Int) : CalculatorAction()
    data class Multiply(val value: Int) : CalculatorAction()
    data class Divide(val value: Int) : CalculatorAction()
}

With CoRedux you can create a store that will be a single source of current Calculator state and will update it on each new incoming CalculatorAction:

val store = coroutineScope.createStore<Int, CalculatorAction>(
    name = "Calculator",
    initialState = 0,
    reducer = { currentState, newAction ->
        when (newAction) {
            is CalculatorAction.Add -> currentState + newAction.value
            is CalculatorAction.Deduct -> currentState - newAction.value
            is CalculatorAction.Multiply -> currentState * newAction.value
            is CalculatorAction.Divide -> currentState / newAction.value
        }
    }
)

Where reducer is a special function responsible for managing state. A reducer specify how the state changes in response to actions sent to the store. Remember that actions only describe what happened, but don't describe how the application's state changes. That is the job of the reducer.

Finally you have to subscribe StateReceiver to the store instance to get the state updates over time:

store.subscribe { state -> updateUI(state) }

StateReceiver is a function that is called whenever the state of the store has been changed. You can think of it as a listener of store's state. More than one StateReceiver can subscribe to the same store instance.

On each new UI intention, UI implementation just need to send (dispatch) it as an action to store instance:

store.dispatch(CalculatorAction.Add(10))
// Current state will become 10
store.dispatch(CalculatorAction.Deduct(1))
// Current state will become 9
store.dispatch(CalculatorAction.Add(1))
// Current state will become 10
store.dispatch(CalculatorAction.Divide(10))
// Current state will become 1

All actions will be processed in serialized order and on each incoming action reducer function is called to compute current state.

createStore has two start modes:

  • with launchMode = CoroutineStart.LAZY (default), Store will wait for first StateReceiver subscription, emit initialState to this StateReceiver and start processing incoming actions.
  • with launchMode = CoroutineStart.DEFAULT, Store will start processing incoming actions immediately. When any StateReceiver subscribe to such store instance, it will receive first state update only on next incoming action.

Side effects

Side effect is an interface with exactly one function - it receives incoming actions, can get current store state at any time and may emit outgoing actions. So basically it is actions in and actions out.

Actions in to side effects are emitted by store after calling reducer function.

Actions out are consumed back again by store and trigger calling reducer function.

Side effects are used to perform additional Job as a reaction on a certain action, for example, making an HTTP request, I/O operations, writing to database and so on. Since they run in a Job they can run async. Each Job is scoped to the context of the store.

Let's add an side effect that makes a network request each time when action is CalculatorAction.Add and current state is 9. If server responds with http code 200 - side effect should emit CalculatorAction.Deduct(1).

val sideEffect = object : SideEffect<Int, CalculatorAction> {
    override val name: String = "network logger"

    override fun CoroutineScope.start(
        input: SharedFlow<CalculatorAction>,
        stateAccessor: StateAccessor<Int>,
        output: SendChannel<CalculatorAction>,
    ): Job = launch(context = CoroutineName(name)) {
        input.collect { inputAction ->
            if (inputAction is CalculatorAction.Add &&
                stateAccessor() >= 0) {
                launch {
                    val response = makeNetworkCall()
                    if (response == 200) {
                        val outputAction = CalculatorAction.Deduct(1)
                        logger.logSideEffectEvent { LogEvent.SideEffectEvent.DispatchingToReducer(name, outputAction) }
                        output.send(outputAction)
                    }
                }
            }
        }
    }
}

You must register your sideEffect inside createStore(sideEffects = listOf(sideEffect)):

val storeWithSideEffect = coroutineScope.createStore<Int, CalculatorAction>(
    name = "Calculator",
    initialState = 0,
    sideEffects = listOf(sideEffect),
    reducer = { currentState, newAction ->
        when (newAction) {
            is CalculatorAction.Add -> currentState + newAction.value
            is CalculatorAction.Deduct -> currentState - newAction.value
            is CalculatorAction.Multiply -> currentState * newAction.value
            is CalculatorAction.Divide -> currentState / newAction.value
        }
    }
)

When we subscribe to storeWithSideEffect and trigger side effect by dispatching right actions - network request from side effect will be performed:

storeWithSideEffect.subscribe { state -> updateUI(state) }
storeWithSideEffect.dispatch(CalculatorAction.Deduct(1))
// Current state will become -1
storeWithSideEffect.dispatch(CalculatorAction.Add(11))
// Current state will become 10 and network request will happen

CoRedux also provides two simplified implementations of SideEffect that you might find useful:

  • SimpleSideEffect that produces on one input action either one output action or no action:
val sideEffect = SimpleSideEffect<State, Action>("Update on server") {
        state, action, logger, handler ->
    when (action) {
        is Action.Update -> handler {
            val httpCode = sendToServer(action.value)
            if (httpCode == 200) Action.Updated else null
        }
        else -> null
    }
}
  • CancellableSideEffect that cancels previously running Job and starts a new one if the same type of action is dispatched to the store:
val sideEffect = CancellableSideEffect<State, Action>("poll server") {
    state, action, logger, handler ->
    when (action) {
        StartPollingServer -> handler { name, output ->
            openServerPollConnection { update ->
                output.send(Action.PollUpdate(update))
            }
        }
        else -> null
    }
}

Implementation details

Internally CoRedux starts main coroutine ("manager"), that is a single source of current state, which is defined in local scope of coroutine - this allows to prevent any concurrency problems on updating the state. Furthermore "manager" coroutine itself is sequentially:

  • starts all side effects coroutines ("workers"), that listens for incoming actions
  • sets current state to initial state
  • immediately pushes the initial state to the StateReciever (depends on launchMode parameter)
  • starts listening for new actions emitted through store.dispatch(action) and from SideEffects

On each new action "manager" coroutine is sequentially:

  • triggers reducer() to update current state inside "manager" coroutine local scope
  • sends updated current state to all StateReceivers
  • broadcast input action to all side effects "workers" coroutines

If a side effect emits a new action this action is added at the end of the internal queue of actions waiting to be dispatched to reducer and to other side effects.

coredux's People

Contributors

abyrnes avatar artem-zinnatullin avatar arturdryomov avatar befrvnk avatar dependabot-preview[bot] avatar dependabot-support avatar inorichi avatar jamolkhon avatar kbiakov avatar okatrych avatar oldergod avatar pwittchen avatar sockeqwe avatar tapchicoma avatar

Watchers

 avatar

Forkers

orderin

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.