Skip to content

Experimental multiplatform port of "Reduks: A "batteries included" port of Reduxjs for Kotlin+Android"

Notifications You must be signed in to change notification settings

patjackson52/Reduks

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Kotlin 1.2.21 Slack channel Android Arsenal

NOT MAINTAINED - LOOK AT www.reduxkotlin.org FOR A MULTIPLATFORM REDUX LIBRARY

EXPERIMENTAL Multiplatform Reduks Fork

This is a experimental fork of the Kotlin "Reduks" 4.x branch altered to support multiplatform projects. Currently only the core module is ported over. Included is a basic sample for Android & iOS.

MPP concerns

  • sycronized block in SimpleStore of original project - this was removed due to lack of "sycronized" support on MPP. This could be an issue if actions are dispatched from different threads. Possible solutions: mutex.lock(), actors, or have the store implement CoroutineContext.
  • original project uses a Java library for persistent collections for performance reasons. Currently there is not a stable kotlin MPP library, however there is one in the works (here)[https://github.com/Kotlin/kotlinx.collections.immutable]

Browse the Docs!

For the full documentation see here, or take a look at the summary.

Reduks modules in brief

Here is a bird-eye view of Reduks modules and features

reduks-core

Reduks core implements all basic standard ReduxJS components and interfaces:

  • Store, Store Creator, Store Subscriber
  • Middlewares, Store Enhancers
  • Thunk Middleware, Standard Action

reduks-core also contains a basic implementation of the Store interface, called SimpleStore, whose behavior is the most similar to the Store in the original ReduxJS implementation In addition to all standard ReduxJS stuff,reduks-core also contains several extensions

  • Store Subscriber Builder and reselect library: this is one most important components of reduks
  • Basic support for Reduks Modules (reduks substates): reduks modules is the reduks way to support modular UI components

dependencies for gradle for reduks-core

// First, add JitPack to your repositories
repositories {
    ...
    maven { url "https://jitpack.io" }
}

// main reduks package
compile 'com.github.beyondeye.reduks:reduks-core:<Reduks_Version>'

Some notable features:

Table of Contents

dependencies for gradle

// First, add JitPack to your repositories
repositories {
    ...
    maven { url "https://jitpack.io" }
}

// main reduks package
compile 'com.github.beyondeye.reduks:reduks-core:2.0.0b12'

//rx-java based state store+ additional required dep for android support
compile 'com.github.beyondeye.reduks:reduks-rx:2.0.0b12'
compile 'com.github.beyondeye.reduks:reduks-android:2.0.0b12'

//kovenant based state store and Async Action Middleware
compile 'com.github.beyondeye.reduks:reduks-kovenant:2.0.0b12'
compile 'com.github.beyondeye.reduks:reduks-android:2.0.0b12'


//dev tools
compile 'com.github.beyondeye.reduks:reduks-devtools:2.0.0b12'

//immutable collections
compile 'com.github.beyondeye.reduks:reduks-pcollections:2.0.0b12'

//reduks bus
compile 'com.github.beyondeye.reduks:reduks-pcollections:2.0.0b12'
compile 'com.github.beyondeye.reduks:reduks-bus:2.0.0b12'

An introduction to Reduks

Reduks (similarly to Reduxjs) is basically a simplified Reactive Functional Programming approach for implementing UI for Android

A very good source of material for understanding redux/reduks are the official reduxjs docs, but I will try to describe here the main principles, and how they blend with Android and Kotlin

Reduks main components are:

  • the State: it is basically the same as the Model in the standard MVC programming paradigm
  • State change subscribers: their purpose is similar to Controllers in MVC
  • Actions and Reducers: Reducers are (pure)functions that specify how the State change in response to a stream of events (Actions)
  • Middlewares: additional pluggable layers (functions) on top of Reducers for implementing logic for responding to the stream of events (Actions) or even modify them before they reach the reducers that implement the State change logic. Middlewares (together with event change subscribers) have also the main purpose to allow implementing 'side effects', that are prohibited in reducers, that must be pure functions.

There is also an additional component that is called the Store but it is basically nothing more than the implementation details of the "glue" used to connect all the other components.

Its responsibilities are

  • Allows access to the current state
  • Allows to send update events to the state via dispatch(action)
  • Registers and unregister state change listeners via subscribe(listener)

The implementation details of the Store and their variations can be quite important, but for understanding Reduks, we can start by focusing first on the other components

This is Reduks in brief. let us now discuss it more in detail

The State

The state is the set of data that uniquely identify the current state of the application. Typically in Android, this is the state of the current Activity.

An important requirement for the data inside the state object is that it is required to be immutable, or in other words it is prohibited to update the state directly.

The only way to mutate the state is to send an action via the store dispatch method, to be processed by the registered state reducers(more on this later), that will generate a new updated state.

The old state must be never modified.

In Kotlin we will typically implement the state as a data class with all fields defined as val's (immutable)

Example:

data class LoginActivityState(val email:String, val password:String, val emailConfirmed:Boolean)

Why using a data class? Because it makes it easier to implement reducers, thanks to the autogenerated copy() method.

But if you don't want to use data classes you can easily implement the copy() method like this:

fun copy(email: String?=null, password:String?=null, emailConfirmed:Boolean?=null) =
     LoginActivityState(email ?: this.email,  password ?: this.password,  emailConfirmed ?: this.emailConfirmed)

State Change Subscribers

Before we discuss how the state changes, let's see how we listen to those changes. Through the store method

 fun subscribe(storeSubscriber: StoreSubscriber<S>): StoreSubscription

we register callbacks to be called each time the state is modified (i.e. some action is dispatched to the store).

val curLogInfo=LoginInfo("","")
val subscriber=StoreSubscriberFn<LoginActivityState> {
    val newState=store.state
    val loginfo=LoginInfo(newState.email,newState.password)
    if(loginfo.email!= curLogInfo.email||loginfo.password!= curLogInfo.password) {
        //log info changed...do something
        curLogInfo= loginfo
    }
}

You should have noticed that in the subscriber, in order to get the value of the newState we need to reference our store instance. You shoud always get a reference to the new state at the beginning of the subscriber code and then avoid referencing store.state directly, otherwise you could end up using different values for newState

Notice that we cannot subscribe for changes of some specific field of the activity state, but only of the whole state.

At first this seems strange. But now we will show how using some advanced features of Reduks, we can turn this into an advantage. The idea behind Reduks is that all that happens in the application is put into a single stream of events so that debugging and testing the application behavior is much easier.

Being a single stream of events we can apply functional programming ideas to application state changes that also make the behaviour of the application more easy to reason about and allows us to avoid bugs.

Reduks allows all this but also working with state changes in a way very similar to traditional callbacks. This is enabled by Reduks selectors: instead of writing the subscriber as above we can write the following code:

val subscriberBuilder = StoreSubscriberBuilderFn<ActivityState> { store ->
    val sel = SelectorBuilder<ActivityState>()
    val sel4LoginInfo=sel.withField { email } .withField { password }.compute { e, p -> LoginInfo(e,p)  }
    val sel4email=sel.withSingleField { email }
    StoreSubscriberFn {
        val newState=store.state
        sel4LoginInfo.onChangeIn(newState) { newLogInfo ->
            //log info changed...send to server for verification
            //...then we received notification that email was verified
            store.dispatch(Action.EmailConfirmed())
        }
        sel4email.onChangeIn(newState) { newEmail ->
            //email changed : do something
        }

    }
}

There are a few things to note in this new version of our sample subscriber:

  • We are creating a StoreSubcriberBuilderFn that a takes a Store argument and returns a StoreSubscriber. This is actual the recommended way to build a subscriber. The StoreSubscriberBuilderFn takes as argument the store instance, so that inside the subscriber we can get the newState and dispatch new actions to the store.
  • We are creating selector objects: their purpose is to automatically detect change in one or more state fields and lazily compute a function of these fields, passing its value to a lambda when the method onChangeIn(newState) is called.

As you can see the code now looks similar to code with Callbacks traditionally used for subscribing to asynchronous updates. Selectors can detect quite efficiently changes in the state, thanks to a technique called memoization that works because we have embraced immutable data structures for representing the application state

Look here for more examples on how to build selectors.

Actions and Reducers

As we mentioned above, whenever we want to change the state of the application we need to send(dispatch) an Action object, that will be processed by the Reducers, that are pure functions that take as input the action and the current state and outputs a new modified state. An action object can be literally any object. For example we can define the following actions

class LoginAction {
    class EmailUpdated(val email:String)
    class PasswordUpdated(val pw:String)
    class EmailConfirmed
}

Reducers

a sample Reducer can be the following

val reducer = ReducerFn<LoginActivityState> { state, action ->
    when(action) {
        is LoginAction.PasswordUpdated -> state.copy(password = action.pw)
        is LoginAction.EmailUpdated -> state.copy(email = action.email,emailConfirmed = false)
        is LoginAction.EmailConfirmed -> state.copy(emailConfirmed = true)
        else -> state
    }
}

Reducers must be pure functions, without side-effects except for updating the state. In particular in a reducer you cannot dispatch actions

Better Actions with Kotlin sealed classes

You may have noticed a potential source of bugs in our previous reducer code. There is a risk that we simply forget to enumerate all action types in the when expression.

We can catch this type of errors at compile time thanks to Kotlin sealed classes. So we will rewrite our actions as

sealed class LoginAction {
    class EmailUpdated(val email:String) :LoginAction()
    class PasswordUpdated(val pw:String) :LoginAction()
    class EmailConfirmed :LoginAction()
}

and our reducer as

val reducer = ReducerFn<ActivityState> { state, action ->
    when {
        action is LoginAction -> when (action) {
            is LoginAction.PasswordUpdated -> state.copy(password = action.pw)
            is LoginAction.EmailUpdated -> state.copy(email = action.email, emailConfirmed = false)
            is LoginAction.EmailConfirmed -> state.copy(emailConfirmed = true)
        }
        else -> state
    }
}

The compiler will give us an error if we forget to list one of LoginAction subtypes in the when expression above. Also we don't need the else case anymore (in the more internal when) Note that the exhaustive check is activated only for when expressions, i.e. when we actually use the result of the when block, like in the code above.

Even Better Actions with Reduks StandardAction

Reduks StandardAction is a base interface for actions that provide a standard way to define actions also for failed/async operations:

 interface StandardAction {
     val payload: Any?
     val error: Boolean
 }

We can use this to rewrite our actions as

 sealed class LoginAction2(override val payload: Any?=null,
                           override val error:Boolean=false) : StandardAction {
     class EmailUpdated(override val payload:String) : LoginAction2()
     class PasswordUpdated(override val payload:String) : LoginAction2()
     class EmailConfirmed(override val payload: Boolean) : LoginAction2()
 }

Notice that we can redefine the type of the payload to the one required by each action type, without even using generics.

Also we redefine the state as

data class LoginActivityState2(val email: String,
                          val password: String,
                          val emailConfirmed: Boolean,
                          val serverContactError:Boolean)

And here is our new reducer that handle server errors

val reducer2 = ReducerFn<LoginActivityState2> { s, a ->
    when {
        a is LoginAction2 -> when (a) {
            is LoginAction2.PasswordUpdated ->
                s.copy(password = a.payload,serverContactError = false)
            is LoginAction2.EmailUpdated -> 
                s.copy(email = a.payload, emailConfirmed = false,serverContactError = false)
            is LoginAction2.EmailConfirmed ->
                if(a.error)
                    s.copy(serverContactError = true)
                else
                    s.copy(emailConfirmed = a.payload)
        }
        else -> s
    }
}

Combining Reducers

When your application start getting complex, your reducer code will start getting difficult too manage. To solve this problem, Reduks provide the method combineReducers that allows splitting the definition of the reducer and even put each part of the definition in a different file.

combineReducers takes a list of reducers and return a reducer that apply each reducer in the list according to the order in the list. For example:

class Action
{
    class IncrA
    class IncrB
}
data class State(val a:Int=0,val b:Int=0)
val reducerA=ReducerFn<State>{ state,action-> when(action) {
    is Action.IncrA -> state.copy(a=state.a+1)
    else -> state
}}
val reducerB=ReducerFn<State>{ state,action-> when(action) {
    is Action.IncrB -> state.copy(b=state.b+1)
    else -> state
}}

val reducerAB=ReducerFn<State>{ state,action-> when(action) {
    is Action.IncrA -> state.copy(a=state.a*2)
    is Action.IncrB -> state.copy(b=state.b*2)
    else -> state
}}
val reducercombined= combineReducers(reducerA, reducerB, reducerAB)

Then for action sequence

IncrA, IncrB

starting from the initial state

State(a=0,b=0)

the combined reducer will produce the finale state

State(a=2,b=2)

Note that this is different from how it works in reduxjs combineReducers. The original reduxjs concept has been implemented and extended in Reduks Modules (see below)

If I feel like I want to dispatch from my Reducer what is the correct thing to do instead?

This is one of the most typical things that confuse beginners.

For example let's say that in order to verify the email address at user registration we must

  • make some server API call (that can fail)
  • and then wait for some notification from the server that the user successfully confirmed the email address (or not).

So we can think of defining the following actions

  • class LoginApiCalled
  • class LoginApiFailed
  • class LoginEmailConfirmed

It is natural to think, when receiving the action LoginApiCalled in the reducer, to add there the relevant logic for this action, namely checking if the call failed, and if the email was confirmed.

Another common related mistake it is to split the logic between multiple store subscribers, for example, in a subscriber that listen for loginApiCalled state changes to add logic for treating api failed.

If you find yourself in this situation then you should defer dispatching an action when you actually have the result of the whole chain (so in our example dispatching only the action LoginEmailConfirmed). At a later stage you can eventually also split the chain into multiple actions (so that you can update the UI at different stages of the user authentication process), but always keep the chain logic in the original place. We will discuss later the Thunk middleware and AsyncAction middleware that will help you handle these chains of actions better

Reduks Modules

TODO

Combining Reduks modules

TODO

Immutable (Persistent) Collections with Reduks

A critical component, from a performance point of view, when defining complex Reduks states are so called persistent collections , that is collections that when modified always create a copy of the original collection, with efficient data sharing mechanims between multiple versions of the modified collections. Unfortunately there are not yet persistent collection in kotlin standard library (there is a proposal). But there are several implementations of persistent collections for the JVM. Some notable ones

  • capsule from the author of the CHAMP state of the art algorithm.
  • Dexx: mainly a port of Scala collections to Kotlin.
  • Paguro: based on Clojure collections (formerly known as UncleJim).
  • PCollections. For a discussion of performance of various implementations see here. Currently the reduks-pcollections module include a stripped down version of the pcollections library (only Pmap and PStack). Although it is not the most efficient implementation, it is not too far behind for maps, it has low method count and play well with standard Java collections. It is used as the building block for reduks bus

Reduks on Android

Activities

Fragments

Saving and Restoring Reduks state on Device Configuration Changes

Reduks bus: a communication channel between fragments

The official method in Android for communicating results from a fragment to the parent activity or between fragments are callback interfaces. This design pattern is very problematic, as it is proven by the success of libraries like Square Otto and GreenRobot EventBus. Reduks architecture has actually severally things in common with an event bus

So why not implementing a kind of event bus on top of Reduks? This is what the BusStoreEnhancer is for. It is not a real event bus, but it is perfectly fit for the purpose of communicating data between fragments (and more). Let's see how it is done. Let's say for example that our state class is defined as

data class State(val a:Int, val b:Int)

with actions and reducers defined as follows

class Action
{
    class SetA(val newA:Int)
    class SetB(val newB:Int)
}
val reducer = ReducerFn<State> { state, action ->
    when (action) {
        is Action.SetA -> state.copy(a= action.newA)
        is Action.SetB -> state.copy(b= action.newB)
        else -> state
    }
}

In order to enable support for reduks bus, your Reduks state class need to implement the StateWithBusData interface:

data class State(val a:Int, val b:Int, 
    override val busData: BusData = BusData.empty) :StateWithBusData
{
    override fun copyWithNewBusData(newBusData: BusData): StateWithBusData = copy(busData=newBusData)
}

Basically we add a busData field (that is a persistent map) and we define a method that Reduks will use to create a new state with an updated version of this busData (something similar of the standard copy() method for data classes, which is actually used for implementation in the example above). The next change we need is the when we create our store. Now we need pass an instance of BusStoreEnhancer:

 val initialState=State(0,0)
 val creator= SimpleStore.Creator<AState>()
 val store = creator.create(reducer, initialState, BusStoreEnhancer())

That's it! Now you can add a bus subscriber for some specific data type that is sent on the bus. For example for a subscriber that should receive updates for

class LoginFragmentResult(val username:String, val password:String)

you add a subscriber like this

store.addBusDataHandler { lfr:LoginFragmentResult? ->
    if(lfr!=null) {
        print("login with username=${lfr.username} and password=${lfr.password} and ")
    }
}

Simple! Note that the data received in the BusDataHandler must be always define as nullable. The explanation why in a moment (and how Reduks bus works under the hood). But first let's see how we post some data on the bus:

 store.postBusData(LoginFragmentResult(username = "Kotlin", password = "IsAwsome"))

That's it. See more code examples here. For the full Api see here

Reduks bus under the hood

What's happening when we post some data on the bus? What we are doing is actualling dispatching the Action

class ActionSendBusData(val key: String, val newData: Any)

with the object class name as key (this is actually customizable) and object data as newData. This action is automatically intercepted by a reducer added by the BusStoreEnhancer and translated in a call to copyWithNewBusData for updating the map busData in the state with the new value. The bus data handler that we added in the code above is actually a store subscriber that watch for changes (and only for changes) of data in the busData map for the specific key equal to the object class name. As you can see what we have implemented is not really an Event Bus, because we do not support a stream of data, we only support the two states:

  • some data present for the selected key
  • no data present

the no data present state is triggered when we call

store.clearBusData<LoginFragmentResult>()

that will clear the bus data for the specified object type and trigger the bus data handler with a null value as input.

Reduks bus in Android Fragments

Finally we can show the code for handling communication between a Fragment and a parent activity, or another Fragment. We assume that the parent activity implement the ReduksActivity interface

interface  ReduksActivity<S> {
       val reduks: Reduks<S>
   }

For posting data on the bus the fragment need to obtain a reference to the Reduks object of the parent activity. You can get it easily from the parent activity for example by defining the following extension property in the fragment

    fun Fragment.reduks() =
            if (activity is ReduksActivity<*>)
                (activity as ReduksActivity<out StateWithBusData>).reduks
            else null

and then we can use it

class LoginFragment : Fragment() {
    fun onSubmitLogin() {
        reduks()?.postBusData(LoginFragmentResult("Kotlin","IsAwsome"))
    }
}

And in another fragment (or in the parent activity) we can listen for data posted on the bus in this way

class LoginDataDisplayFragment : Fragment() {
    override fun onAttach(context: Context?) {
        super.onAttach(context)
        reduks()?.addBusDataHandler(tag) { lfr:LoginFragmentResult? ->
            if(lfr!=null) {
                print("login with username=${lfr.username} and password=${lfr.password} and ")
            }
        }
    }

    override fun onDetach() {
        super.onDetach()
        reduks()?.removeBusDataHandlersWithTag(tag) //remove all bus data handler attached to this fragment tag
    }
}

Notices that we are using the Fragment tag (assuming it is defined) for automatically keeping track of all registered bus data handlers and removing them when the Fragment is detached from the activity for the full source code of the example discussed see here

Middlewares

TODO

Thunk Middleware

TODO

Promise Middleware

TODO

Logger Middleware

TODO

Types of Reduks Stores

TODO

Simple Store

TODO

RxJava based Store

TODO

Promise based (Kovenant) Store

TODO

Store Enhancers

TODO

Reduks DevTools

TODO

Open source library included/modified or that inspired Reduks

License

The MIT License (MIT)
Copyright (c) 2016 Dario Elyasy

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

Experimental multiplatform port of "Reduks: A "batteries included" port of Reduxjs for Kotlin+Android"

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Kotlin 100.0%