Skip to content

Latest commit

 

History

History
210 lines (161 loc) · 7.42 KB

README.md

File metadata and controls

210 lines (161 loc) · 7.42 KB

réseau

Download

A highly scalable, reactive, MVVM-like library for Android, powered by Redux, RxJava and Kotlin.

It is inspired by Nubank's Lego and Reduks.

The pattern encourages you to write building blocks —generally composed by a controller, a view binder and a view model—, hereafter called nodes, that satisfy the following principles:

1) The state of a node is stored in an object tree within a single store.
2) The only way to change the state is to emit an action, an object describing what happened.
3) To specify how the state tree is transformed by actions, you write pure reducers.
4) The state of your whole activity can be accessed in a single object tree.

Then you arrange different nodes as in a graph, e.g.:

Example use

Installation

dependencies {
    compile 'com.github.denisidoro:reseau:+' // or x.y.z
}

The pattern in a nutshell

Let's write a simple counter application that consists of a single node.

First, we define a state:

data class CounterState(val i: Int)

Then we define actions and a state reducer:

sealed class CounterActions {
    object DECREMENT
    object INCREMENT
}
val counterReducer = Reducer { state: CounterState, action: Any ->
    when (action) {
        is DECREMENT -> state.copy(i = state.i - 1)
        is INCREMENT -> state.copy(i = state.i + 1)
        else -> state
    }
}

Later on we build a view model, that converts state to view properties:

class CounterViewModel(s: CounterState) : ViewModel {
    val text = s.i.toString()
}

Then we define the view binder:

class CounterViewBinder(...) : ViewBinder<CounterViewModel>(...) {

    init {
        with(root) {
            minusBT.setOnClickListener { dispatch(DECREMENT) }
            plusBT.setOnClickListener { dispatch(INCREMENT) }
        }
    }

    override fun bind(viewModel: CounterViewModel) {
        root.countTV.text = viewModel.text
    }

}

Finally, we create the controller that holds everything together and has all necessary logic:

class CounterController(...) : ViewStoreController<...>(...) {

    override fun createViewBinder(...) = CounterViewBinder(...)
    override fun getInitialState() = CounterState(13)
    override fun getReducer() = counterReducer

    // called when the activity is created
    override fun onCreate() {
        super.onCreate()
        stateObservable
                .map(::CounterViewModel)
                .subscribe { emitViewModel(it) }
    }

}

And connect it to the activity:

class CounterActivity : ControllerActivity<CounterController>() {
    override val layoutRes = R.layout.activity_counter
    override fun createController() = CounterController(this)
}

Combining nodes

Let's reuse the previous counter node and code a screen that has 4 nodes: two independent counters, one for log that keeps track of both values and another one that holds them together, as in the graph below:

Hierarchy

The final result will be as follows:

Demo

In order to have two counter views, we'll change the activity layout XML and pass an argument to the counter controller so that it can map it to the corresponding layout resource ID.

Hierarchy setup

Each controller may have a parent or children.

To setup the desired hierarchy we define the root controller's children like this:

class MultipleController(...) : Controller() {
    override val children = listOf(
            CounterController(activity, 0),
            CounterController(activity, 1),
            LogController(activity))
}

Accessing state from other nodes

It's up to you how to expose state between nodes.

You can either pass a getter lambda function to child controllers or define public functions or even prevent it whatsoever, for encapsulation reasons. One native, quick way to do this is to make your root controller extend RootController and use an extension function that returns the state observable for a given controller by its name.

If we name our controllers accordingly and do the necessary modifications, our log controller could be as follows:

class LogController(...) : ViewController<...>(...) {

    override fun createViewBinder(...) = LogViewBinder(...)

    override fun onStart() {
        super.onStart()
        Observable.combineLatest(
                stateObservableByName<CounterState>("counter0"),
                stateObservableByName<CounterState>("counter1"),
                { c0, c1 -> Pair(c0, c1) })
                .map { LogViewModel(it.first, it.second) }
                .subscribe { emitViewModel(it) }
    }

}

If any exception is thrown in the process, an Observable.error() is returned.

The implementation of the Log view binder and view model are similar to the ones before.

Dispatch range

If we start the app like so, clicking on a button of the second counter will interfere with the value from the first one, because both of them have state that is reduced by the same events. To prevent this, we can restrict the dispatch range:

class CounterController(...) : ViewStoreController<...>(...) {
    // ...
    override val dispatchRange = DispatchRange.SELF
}
  • SELF: the dispatch will possibly reduce only the state of the node that dispatched the action;
  • DOWN: the dispatch will possibly reduce the state of the node that dispatched the action and will be propagated to child nodes downstream;
  • TOP_DOWN (default): the dispatch will possibly reduce the state of the root node and will be propagated to child nodes downstream.

Controller types

  • Controller: simple, stateless, it has no view;
  • ViewController: stateless, it represents a view with a view binder and a view model;
  • ViewStoreController: same as above, but stateful;
  • RootController: stateless by default, it can represent the state of your activity in a single object tree.

What about all those redux libraries I know and love?

RxJava operators like map(), filter(), debounce(), combineLatest() or distinctUntilChanged() can replace some libraries such as reselect or redux-debounce.

Trivia

Etymology

< re, as in redux, reactive; and réseau, which means network in French.

To do

  • Tests
  • Publish lib
  • Implement middlewares