Reduks


Source link: https://github.com/beyondeye/Reduks

Notice about Reduks 2.x and Reduks 3.x

The latest version of reduks 2.x (current master branch) is 2.0.0b12. I am currently working on a major new release (3.x_kotlin_1_1 branch) with kotlin coroutine support and a lot of new features. I will probably officially switch to 3.x in master branch once that coroutines exit experimental status, but there are already build availables for 3.x. The latest build is 3.0.0b1. But beware that the documentation has not been updated yet for 3.x

Reduks: a port of Reduxjs for Kotlin+Android

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

Reduks Activity

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 store enhancer (see below)

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: PMap<String, Any> = emptyBusData()) :StateWithBusData  {

  override fun copyWithNewBusData(newBusData: PMap<String, Any>): 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()?.addBusDataHandlerWithTag(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

Persisting Reduks state and activity lifecycle

TODO

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. 

Resources

AdvancedTextView provides advanced features that simplifies complicated UI processes.

This android library lets you persist the state of your views to recover them later in order to display old data until fresh data arrives.

RxGroups lets you group RxJava Observables together in groups and tie them to your Android lifecycle.

Step indicator for onboarding or simple viewpager.

Carousel banner view.

A simple radar chart.

Topics


2D Engines   3D Engines   9-Patch   Action Bars   Activities   ADB   Advertisements   Analytics   Animations   ANR   AOP   API   APK   APT   Architecture   Audio   Autocomplete   Background Processing   Backward Compatibility   Badges   Bar Codes   Benchmarking   Bitmaps   Bluetooth   Blur Effects   Bread Crumbs   BRMS   Browser Extensions   Build Systems   Bundles   Buttons   Caching   Camera   Canvas   Cards   Carousels   Changelog   Checkboxes   Cloud Storages   Color Analysis   Color Pickers   Colors   Comet/Push   Compass Sensors   Conferences   Content Providers   Continuous Integration   Crash Reports   Credit Cards   Credits   CSV   Curl/Flip   Data Binding   Data Generators   Data Structures   Database   Database Browsers   Date &   Debugging   Decompilers   Deep Links   Dependency Injections   Design   Design Patterns   Dex   Dialogs   Distributed Computing   Distribution Platforms   Download Managers   Drawables   Emoji   Emulators   EPUB   Equalizers &   Event Buses   Exception Handling   Face Recognition   Feedback &   File System   File/Directory   Fingerprint   Floating Action   Fonts   Forms   Fragments   FRP   FSM   Functional Programming   Gamepads   Games   Geocaching   Gestures   GIF   Glow Pad   Gradle Plugins   Graphics   Grid Views   Highlighting   HTML   HTTP Mocking   Icons   IDE   IDE Plugins   Image Croppers   Image Loaders   Image Pickers   Image Processing   Image Views   Instrumentation   Intents   Job Schedulers   JSON   Keyboard   Kotlin   Layouts   Library Demos   List View   List Views   Localization   Location   Lock Patterns   Logcat   Logging   Mails   Maps   Markdown   Mathematics   Maven Plugins   MBaaS   Media   Menus   Messaging   MIME   Mobile Web   Native Image   Navigation   NDK   Networking   NFC   NoSQL   Number Pickers   OAuth   Object Mocking   OCR Engines   OpenGL   ORM   Other Pickers   Parallax List   Parcelables   Particle Systems   Password Inputs   PDF   Permissions   Physics Engines   Platforms   Plugin Frameworks   Preferences   Progress Indicators   ProGuard   Properties   Protocol Buffer   Pull To   Purchases   Push/Pull   QR Codes   Quick Return   Radio Buttons   Range Bars   Ratings   Recycler Views   Resources   REST   Ripple Effects   RSS   Screenshots   Scripting   Scroll Views   SDK   Search Inputs   Security   Sensors   Services   Showcase Views   Signatures   Sliding Panels   Snackbars   SOAP   Social Networks   Spannable   Spinners   Splash Screens   SSH   Static Analysis   Status Bars   Styling   SVG   System   Tags   Task Managers   TDD &   Template Engines   Testing   Testing Tools   Text Formatting   Text Views   Text Watchers   Text-to   Toasts   Toolkits For   Tools   Tooltips   Trainings   TV   Twitter   Updaters   USB   User Stories   Utils   Validation   Video   View Adapters   View Pagers   Views   Watch Face   Wearable Data   Wearables   Weather   Web Tools   Web Views   WebRTC   WebSockets   Wheel Widgets   Wi-Fi   Widgets   Windows   Wizards   XML   XMPP   YAML   ZIP Codes