Adnan wroteBut what would be the added benefit of Rx over Redux? Can they be used simultaneously?
Redux is a pattern, whereas Rx is a tool, that replaces interfaces and listeners with event streams, in order to replace pulling data with pushing data to your UI, and help code in a reactive way, rather than in an imperative way.
This won't make sense without an example, so here we go. I'll use Kotlin cause it has sealed classes, which helps a lot.
So you already know Redux, it's quite simple as a concept. Now if you add Rx to it, you will have RxFeedback, which is basically a state machine, similar to redux, but that also monitors the state itself, to fire more events when needed.
So let's take Bluetooth Low Energy (I'll refer to this as BLE) scan as an example. For BLE scan to work an Android, you need all of these conditions, plus one that I will add as a requirement for our example app:
- Location permission (idk why Android needs location for BLE to work)
- Location needs to be ON
- Bluetooth permission
- Bluetooth needs to be ON
- Requirement from the app: the user needs to be on a certain screen, and not scan for devices all the time.
So here's the cool thing about this, I can basically encode the problem directly into a state class, like so:
data class AppState(
var isLocationPermissionGranted: Boolean = false,
var isLocationEnabled: Boolean = false,
var isBluetoothPermissionGranted: Boolean = false,
var isBluetoothEnabled: Boolean = false,
var isScanEnabled: Boolean = false, //our app requirement to only scan on certain screens
var scanResults: List<ScanResult> = listOf()
)
Where RxFeddback will help, is that the fields above will be updated Asynchronously, since you don't really have any control over what the user will do. He can grant permissions, then turn on hardware, or vise-versa. he can go to the scan screen before being ready to scan. The important thing is for the app to react correctly. Doing this without Rx and Redux would be a nightmare. Imagine having to manage all these async events through callbacks!
So now comes the fun part. We need to write a system, that monitors the hardware availability, the screen the user is on, and react accordingly. So we need to write events, and feedbacks.
Here are the events that will happen in the app
sealed class AppEvent{
//using data class here is better for logging
//it's good for whenever one of the events carries data
//Bluetooth adapter events
data class BluetoothPermission(val granted: Boolean): AppEvent()
data class BluetoothStateChanged(val enabled: Boolean) :AppEvent()
//Location adapter events
data class LocationPermission(val granted: Boolean): AppEvent()
data class LocationStateChanged(val enabled: Boolean): AppEvent()
//scan events
data class EnableOrDisableScan(val enable: Boolean) : AppEvent()
data class FoundDevices(scanResults: List<ScanResult>): AppEvent()
}
our Reducer is pretty straight forward also
object Reducer{
//take an oldState, handle and event, and produce a newState as a result
fun reduce(oldState: AppState, event: AppEvent): AppState{
//logging here would be extremeluy helpful. This is what I meant in the original post that using
//this pattern makes logging very clear. Since any app event will pass by this code. you can
//easily monitor what is going on in the App, without even looking at the UI!
Log.d("AppEvent","$event")
val newState = when(event){ //this is like switch case, to handle each type of events
//copy the oldState which will be our new state, after applying a certain mutation, in the incoming apply block
is AppEvent.BluetoothPermission -> oldState.copy().apply{
//simply mutate the state by grabbing the value from the event
isBluetoothPermissionGranted = event.granted
}
is AppEvent.BluetoothStateChanged -> oldState.copy().apply{
isBluetoothEnabled = event.enabled
}
is AppEvent.LocationPermission -> oldState.copy().apply{
isLocationPermissionGranted = event.granted
}
is AppEvent.LocationStateChanged -> oldState.copy().apply{
isLocationEnabled = event.enabled
}
is AppEvent.EnableOrDisableScan -> oldState.copy().apply{
isScanEnabled = event.enable
}
is AppEvent.FoundDevices -> oldState.copy().apply{
scanResults = event.scanResults//this logic might be more complex, this is just an example.
}
}
}
}
now in order to enable/disable scan, we need to monitor the state. This is called querying, and so we write queries. Fortunately, swift and kotlin allow us to write extensions functions. So we can store them outside of the class file, but still call them via `this`
The queries need to return the data that the system needs to do some effect. For example, to start a scan, all of the above conditions need to be met, and we will only scan for 5 seconds, so the query will return 5
What I like to do is store queries in a file like AppStateQueries.kt
fun AppState.enableScan():Optional<Long>{ //I'll explain later why this needs to be an Optional
//inside this extention is as if I was inside of the AppState class
// so I can directly access AppState fields
return if(isLocationPermissionGranted
&&isLocationEnabled
&&isBluetoothPermissionGranted
&&isBluetoothEnabled
&&isScanEnabled){
Optional.Some(5) //scan for 5 seconds
}else{
Optional.None() //do nothing
}
}
Ok now for the last part. The System that will tie all of these together. Using what's called a feedback loop.
A feedback is basically this type
typealias Feedback = (ObservableSchedulerContext<AppState>) -> io.reactivex.Observable<AppEvent>
It looks weird, I know, but that's the case with all functional programing at first.
(ObservableSchedulerContext<AppState>) -> io.reactivex.Observable<AppEvent> basically means, a function that transforms a stream of State into a stream of Events. Which is what we are trying to do. As soon as our state reaches a point where it can scan for BLE devices, it will fire the appropriate event.
Now to create the System, we need to provide 3 things:
- An initial state, a reducer, and list of feedbacks.
I'll now explain how to build the System, in a class called AppSystem
//class declaration
fun initSystem(): Observable<AppState>{
return Observables.System( //Observables is class provided in the RxFeedback library
initialState = AppState(), // We declared AppState class with all default values, so () is enough here
Reducer::reduce, // a pointer to the reduce7 function, to be used by the System
//now for a list of feedbacks
scanFeedback,
externalEventsBindings // more on this next
)
}
//now to declare feedbacks
// there are 2 ways to do this, either using the react<State,Any,Event>() class, to react to a certain state
// or by using the bind operator, to create a feedback using external events, like Bluetooth and location
//so lets first handle bluetooth and location, for that we need to monitor the hardware and permission
// I will write pseudo-code for this just to give and idea
//wrap a listener into an observable
private val bleStateObservable = Observable.Create<AppEvent>{ emitter ->
//assuming we have some helper to monitor BLE
BleHelper.monitorBleState(object: OnBleStateChangedListener(){
@override fun onBleAdapterStateChanged(enabled: Boolean){
//push and event into the stream
emitter.onNext(AppEvent.BluetoothStateChanged(enabled))
}
@override fun onBlePermissionsChanged(enabled: Boolean){
//push and event into the stream
emitter.onNext(AppEvent.BluetoothPermission(enabled))
}
})
}
// same for Location
private val locationStateObservable = // ...
//bind the external event sources to the system
private fun externalEventsBindings() = bind<AppState, AppEvent> {
Bindings(
subscriptions = listOf(),
events = listOf(
bleStateObservable,
locationStateObservable
)
)
}
//our last feedback reacts directly to our app state itself, not an external source
// it's like saying, because I have all of the conditions available, I should start a scan
//so in code, it looks like this
private val scanFeedback = reactc<AppState,Long,AppEvent>( //react to an appstate, query a long value, and fire an AppEvent as a result
query{//query is what we monitor from the state. Here we query the readiness to scan, waiting for a Long value, which is the duration to scan for
it.enableScan()//the query we wrote earlier
},
effects{ scanDuration: Long ->
//now that we have the duration, we can scan
//assuming such function exists, and that it takes a listener to callack with the scan results. In Kotlin we can use Lambdas
BleHelper.scan(scanDuration){ scanResults -> //listener shortened into a lambda
AppEvent.FoundDevices(scanResults)
}
}
)
So now, as soon as you call initSystem(), you will get a state observable, subscribe to that, and the system will run, and provide state updates to the UI.
For example, I can now update a screen with a list of devices, or show a warning that the BLE adapter isn't ready, simply by subscribing to the AppState stream, e.g the system we just created
//inside some activity ...
private fun subscribeToAppState(): List<Disposable>{
val disposables = mutableListOf<Disposable>()
val appStateObservable = AppSystem.appStateObservable // provided I stored it in a static field
//now we update UI based on state pushes
//show a list of devices
disposables.add(
appStateObservable.map{ it.scanResults }.subscribe{ updateSomeListAdapter(it) }
)
//show warning
disposables.add(
appStateObservable.map{it.isLocationPermissionGranted}.subscribe{
/*show a warning if not granted, it is best to not compute anything here, and put such code in a ViewModel
as to keep your UI code as concise and maintainable as possible
*/
})
//etc ...
return disposables //don't forget to dispose these at activity pause to prevent UI crashes
}
I hope this covers up your question, and I hope the example is as clear as I think it is in my head. I tried to explain it all as fast as I can.
It may seem like overhead at first, but trust me, we couldn't get BLE to work before this. The state, conditions and requirements I demoed are like 10th of what our app needs to do, in terms of BLE alone, now add Camera, wifi, user clicks, etc... And you know it would be a mess without this sort of organization.
Plus, there are perks of using this structure, which are that anyone who takes your code-base and wants to continue on it, can easily open just the event and state classes, and understand rapidly what needs to be done. Then to debug, all he has to do is debug a small part of the reducer that is failing...
Bonus:
- Why use Optional<*>?
The Feedback system works by querying your state each time it changes, in order to fire events any time it needs to. Once it does, the system subscribes to the Observable<Event> that is created, and keeps the subscription as long as the value remains the same.
This is perfect for when a user spams the scan button for example. Even if 10000 scan events fly, the value of the query will remain the same, and your system will only create one Observable and subscribe to that, so the spam the user did will have no effect.
Whenever the query returns an Optional.None, the system unsubscribes from that stream.
It is basically a way to say: yes I want the event to happen/ No, I do not want the event to happen. Using Optionals
- Some things that need care
The system observable needs at least one subscription to stay "alive" and working. It also needs to be shared and replayed, so any UI that attaches to it any point can read from it the last value. I can't share that code, I hope you understand, but if you ever try this out, you'll know what I'm talking about and should be able to fix it.
You can bind to a events subject in order to push events from the UI into the system, using the bind operator I demoed to wrap BLE state events.