Skip to content

A component-based, global-state, reactive library for Android. Built using rx (kotlin, android)

Notifications You must be signed in to change notification settings

GuyMichael/Reactdroid

Repository files navigation

Reactdroid

Reactdroid is a reactive component-based MVI architecture, along with a Flux, global-state architecture (2 library cores). It is a comprehensive architecture to creating modern mobile apps.

Reactdroid makes it extremely easy to build a modern app. With clear contracts between components, clear and easy managed app state, standardized coding, and predictable, bug free app flow with very fast development times.

Note: while it is currently built for Android, its core is pure Kotlin (and RxKotlin) and is destined to be separated from this project, to serve iOS (ReactIOs) as well, using Kotlin Multiplatform. Any developer that would like to take part in this project, please contact me. The MVI core is built using pure Kotlin. The Flux core also uses RxKotlin for Store management. The Android layer on top contains Android UI Component implementations, such as List, Text and more, and uses RxAndroid, mainly to support Android's MainThread.

Quick Start

For a fully working app which uses this library, just go here

For a deeper explanation of the architecture, please read these Medium articles TL;DR: it is similar to React and Redux.

To import the project using Gradle:

implementation 'com.github.GuyMichael.reactdroid:reactdroid:0.1.87'

Below are some (UI) Component examples and explanation to showcase the style of this library.

Components Core ('React' - like) - Overview

Basically, the whole architecture is based on the Component class, that makes it easier to code UI, as everything UI is located inside 1 method only - render(). A Component's 'API' is defined simply by an (complex) Object - OwnProps. By passing (particular-type) OwnProps to some Component, you control it and tell it when and what to render. Simply put - if the new props differ from the previous ones a Component already has - it will (re) render.

An AComponent is the Android's implementation of the Component model. An AComponent wraps an Android View and controlls it. Note: there are also ComponentActivity and ComponentFragment which are extensions to the Android models, to 'convert' their usage to a Component-like one. But you should avoid using them, if possible - you should just wrap your whole Activity/Fragment's layout with an AComponent.

AComponents - Examples

Below is an example showcasing how to wrap an Android TextView with an AComponent, specifically, ATextComponent, from inside another AComponent. Note: withText() makes use of Kotlin Extensions.

//bind views to components.
val cText = withText(R.id.textView)       //cText stands for 'componentText'

//inside the parent component's render method,
// and only here(!) we call 'onRender' for all children.
override fun render() {
    val props = this.props
    
    //standard usage
    cText.onRender(TextProps(props.childTxt))
    
    //OR utility extension
    cText.renderText(props.childTxt)
    
    //OR utility to handle hide when text is null
    cText.renderTextOrGone(props.childTxt)
}

Here is what does a button look like:

val cBtn = withBtn(R.id.button) {
    //onClick - update (own, internal) state
    this.setState(MyState(this.state.counter+1))
}

override fun render() {
    val state = this.ownState
    
    cBtn.renderText(
        if (state.counter > 0)
          "ACTIVE" 
        else "DISABLED"
    )
    
    //normally you won't need to use the View directly, 
    // as the props and utility methods are suitable for
    // most cases. But for the sake of this example,
    // let's use the underlying View to disable the button.
    cBtn.mView.setEnabled(state.counter > 0)
}

A (RecyclerView wrapper) list, to show Netflix titles/movies:

val cList = withList(R.id.recyclerView)

override fun render() {
    cList.onRender( ListProps(
        this.props.netflixTitles
            ?.map { ListItemProps(
                //id.     layout.                 item props.    item AComponent creator
                it.title, R.layout.netflix_title, DataProps(it), ::NetflixTitleItem
            )}
            
            ?.sortedBy { it.props.data.title }
            ?: emptyList()
    ))
}

Note: ListItemProps contains everything for the underlying Adapter to know what to render. There is absolutely no need to have a custom Adapter or ViewHolder(!). You need 2 things: an 'item' layout (xml file) and an 'item' AComponent to wrap it. You can use as many View types and layouts as you like, as well as change them between renders.

Below is an example of a NetflixTitleItem AComponent. Except for a layout xml file, this is the only code you need to render lists in Reactdroid:

class NetflixTitleItem(v: View) : ASimpleComponent< DataProps<NetflixTitle> >(v) {

    private val cTxtName = v.withText(R.id.netflix_title_name)

    override fun render() {
        cTxtName.renderText(this.props.data.title.name)
    }
}

Here is a text input (EditText wrapper). A String input in this case, but you can use an Int input for example - to automatically parse input as numbers into your state.

private val cInput = withStringInput(R.id.editText) {
    //user changed EditText's text. Update (own) state to re-render
    setState(MyState(inputTxt = it))
}

override fun render() {
    cInput.onRender(this.state.inputTxt)
}

Flux core ('Redux' - like) - Overview - Store and global app state

A Store is basically a global app state handler which UI Components can use to update the app state (using a Dispatch call). When that state is updated, the Store notifies all connected Components to 'tell' them to (re) render (update the UI). This way the data flow in the app is uni-directional and also very simple: AComponent -> (Dispatches update Action) -> Store updates its GlobalState -> notifies back to (all connected) AComponent(s)

Here is how to define a global application state, by creating a Store:

object MainStore : AndroidStore(combineReducers(
    FeatureAReducer                                 //you may add for example: MainDataReducer, FeatureBReducer...
))

As you can see, the app's GlobalState consists of (possibly many) Reducers. Each Reducer holds and manages some part of the whole GlobalState and, each part, consists of enum keys that each maps to some specific value. It is most easy to think of a Reducer as a mapping of (state) keys to Objects - Map<String, Any?>. And so, the whole GlobalState can be thought of as a mapping of Reducers to their own maps - Map<Reducer, Map<String, Any?>>.

Let's use the withText() (TextComponent) example from above, but this time we will connect its text to the Store, instead of taking it from its 'parent' props.

Below is how we define a basic Reducer:

object FeatureAReducer : Reducer() {
    override fun getSelfDefaultState() = GlobalState(
        //define the initial state of given keys - for when the app starts
        FeatureAReducerKey.childTxt to "Initial Text"
    )
}


//define the FeatureAReducer keys
// Note: future versions will hopefully make use of Kotlin's Sealed classes, to eliminate the need
//       for using enums in Android and help with having typed keys.
enum class FeatureAReducerKey : StoreKey {
    childTxt    //should map to a String
    ;

    override fun getName() = this.name
    override fun getReducer() = FeatureAReducer
}

Now we have a global app state! Let's see how we can Dispatch 'actions' to change it. We just need to provide the (reducer) key to update, and the (new) value:

MainStore.dispatch(FeatureAReducerKey.childTxt, "Some other text")

Only thing missing is a way to listen (connect) to state changes so that Components will 'know' when to (re) render. Connecting to Store is done by encapsulating an AComponent inside another one - a special Component that handles everything for you. Technically speaking, that (other) Component is a HOC - High Order Component.

Let's connect that TextComponent to the Store:

val cText: withText(R.id.textView)

val connectedCText = connect(
    cText
    
    //mapStateToProps -> a function that creates 'props' from the whole 'GlobalState'
    , { globalState -> TextProps(
          state.get(FeatureAReducerKey.childTxt)
      )}
      
     // Store supplier
    , { MainStore }
)

From now on, cText will be (re) rendered whenever FeatureAReducerKey.childTxt's value is changed.

Note: it's encouraged to define the 'connection' inside a Component's 'companion object' - this way, when you write some custom Component, you also define how to connect to it, in the same file. This is how it will look like:

class MyComponent : ASimpleComponent<MyProps>() {
    override fun render() {
        //update the UI
    }

    companion object {
        fun connected(c: MyComponent): AComponent<EmptyProps> {
            return connect(c, ::mapStateToProps, { MainStore })
        }
    }
}


private fun mapStateToProps(s: GlobalState, p: EmptyProps): MyProps {
    return MyProps(...) //create from 's'
}

And now using the connected version of your MyComponent is super easy:

    private val connectedMyComponent = MyComponent.connected(withMyComponent(R.id.my_component_layout))

    override fun render() {
        connectedMyComponent.onRender()
        //Note: no need to provide 'MyProps'.
        //      the 'connected' component's 'api props' are of type 'EmptyProps' -
        //      it provides the inner component ('MyComponent') with its actual props ('MyProps'),
        //      by using the Store/GlobalState (mapStateToProps)
    }

That's a basic example, but it explains exactly how this Flux architecture works. You Dispatch some Action to the Store (e.g. from your Button Component) and the Store handles the update for you, telling your Component when to (re) render. The mapStateToProps you provide to connect() tells your Component what to render. simple as that.

R8 / ProGuard

No requirements at this stage, except for RxKotlin & RxAndroid related rules which may apply. Please contact me if you encounter any issues.

License

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

About

A component-based, global-state, reactive library for Android. Built using rx (kotlin, android)

Resources

Stars

Watchers

Forks

Packages

No packages published