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.
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.
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.
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)
}
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 map
s - 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.
No requirements at this stage, except for RxKotlin & RxAndroid related rules which may apply. Please contact me if you encounter any issues.
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.