Closed carterhudson closed 6 years ago
Do you handle navigation as a side effect?
Up to you, RxRedux is unopinonated but if you would like to reuse the state machine you better keep the navigation out of the state machine. Typically I would push navigation one step above and if needed navigate on state transition. I.e we do it ViewModel or Presenter like this:
class MyPresenter( private val stateMachine : FooStateMachine, private val navigator : Navigator ) {
init {
val state = stateMachine.state.share()
state.subscribe { state -> view.render() }
state.filter { state -> state is FooState }
.subscribe {
// If we reach Foo state navigate to Foo
navigator.navigateToFoo()
}
}
}
I notice you have the PaginationStateMachine which creates its own store - are you advocating creating a store for every state machine, and a state machine for every view/model?
Yes, I think a single state machine for the whole application is not working out on Android because unlikely to web you have to deal with process death and might have to restore state but also have components like Fragment and Backstack that restore their state automatically. So you could try to use a single state machine for your whole application (RxRedux is not preventing you from doing that) but I think it is less error prone to have a store for each ViewModel. You can connect multiple Stores / StateMachines together if needed but typically in a real world application 2 StateMachines share for example the same Database rather than beeing dependent on each other.
The above would lead to having many stores. If so, wouldn't that would be more Flux than Redux? The number of stores is a key difference between the two.
I wouldn't call it Flux. Although Redux typically only has 1 Store for whole application the key difference to Flux imho is the dispatcher who decides which stores gets which action. A Flux store also doesn't have a Reducer function but rather takes the Action and mutates the state of a Store (so state of store in flux is not immutable, in redux state is immutable). So I would say this library and sample is still Redux (tailored to fit on Android).
If it were a more robust application, would the PaginationStateMachine instead be something like ApplicationStateMachine in order to avoid many stores?
Again, up to you, but I would say no, you have one state machine for each screen / activity / fragment.
There is no concept of middleware here (at least not explicitly named) - are Side Effects intended to act as an Action Creator + Middleware hybrid?
SideEffect
is the Middleware (Redux has no concept of ActionCreator as Flux has).
I hope that helps.
Up to you, RxRedux is unopinonated but if you would like to reuse the state machine you better keep the navigation out of the state machine. Typically I would push navigation one step above and if needed navigate on state transition
I agree with you about the navigation, to a degree. I think of it as a concept rooted purely in whatever framework you're using, and that it probably doesn't need any representation in State. In fact, your example is very close to how I handle navigation with LiveData
and avoid using SingleLiveEvent
. I had the problem where LiveData
would re-issue the state that signaled navigation once it was re-subscribed to. Navigating back would trigger re-subscription to the LiveData
stream, instantly navigating me forward again. Instead of having the view produce an event like "Navigated" in order to have a non-navigation state as the last state pushed through LiveData, I abstracted navigation into a Navigator
class which is injected into the ViewModel
. Actions / Events that indicate navigation should take place are then intercepted, do not get reduced, and do not produce new states. Navigation states never get pushed through LiveData
. So far, it works well for me.
Yes, I think a single state machine for the whole application is not working out on Android because unlikely to web you have to deal with process death and might have to restore state but also have components like Fragment and Backstack that restore their state automatically.
I've had some success with a single application Store in my own projects. I think the failure or success of this approach with Android is largely dictated by how you shape your application state. I've taken to separating view state and having Application State, as managed by the Store, be a normalized Data State. I define a sealed ViewState
class for each view that represents the various states of the view, and I use the Store's "Data State" to create the ViewState. I'm not entirely sure if this is a good practice, but it works so far.
Redux has no concept of ActionCreator as Flux has
Maybe not as a concrete class, but it does have the concept of action creators. I do agree, though, that middleware / side effects are not "action creators" as relates to Redux.
I also thought it was interesting that, in your examples, the SideEffects / Middleware run AFTER the reducer has already disregarded an Action. This seems to be a different order of operations from Redux, and I'm a little curious about the thought process / decision behind that.
Thanks for taking the time to reply. It was very informative and I do appreciate it - I love learning about this stuff.
I also thought it was interesting that, in your examples, the SideEffects / Middleware run AFTER the reducer has already disregarded an Action. This seems to be a different order of operations from Redux, and I'm a little curious about the thought process / decision behind that.
Since an Action
has to go through the reducer anyway and could potentially cause a state change. it is more predictable to do the stat transition first (by the reducer) and then let the SideEffects
do their work. Since SideEffects might access the state (StateAccessor
) it is more predictable and ensures ordering of input events as you always know reducer, then SideEffects (in the order they are passed to .reduxStore()
. This was a explicit design decision.
Interesting. Sounds like that is a key difference between canonical Redux Middleware and RxRedux SideEffects. I'm going to give this a try on my next project :]
@sockeqwe I'm fascinated by your interpretation of Redux architecture on Android, but here's another one small use case I'm concerned about.
How'd you handle the following situation:
Imagine there's CloseTapAction
, triggered immediately after Close button is clicked on the screen, and as a reaction to this action we have to:
a) track this event to analytics;
b) navigate to the previous screen.
According to your navigation approach, we can easily define a sort of QuitScreenState
and return it as a reaction to CloseTapAction
in our reducer. It feels totally ok.
But what about analytics? Should it be considered as a side effect (even though it will be just a single line like analytics.trackCloseEvent()
)? And, if so, which action should this side effect return as a result? Or are there any ways to avoid tons of boilerplate code for these two simple operations?
Thanks in advance!
@constmikhailovskiy
In other state machine libraries that model side effects, such as Mobius, side effects don't have to give rise to other actions or events. In this library, however, it's part of the signature, so I'm not sure. Perhaps you could specify a neutral action like "Idle." If you treat navigation as a side effect, I'd imagine you would need to treat the analytics call as a side effect as well, so that the former doesn't preempt the latter. I can't speak for this library as I haven't used it yet, but I treat analytics the same way I treat navigation: It doesn't mutate state or generate new inputs, so it doesn't need to be dispatched for handling or modeled as a side effect. I'm still on the fence about how "pure" I want to be with regard to events being selectively dispatched to my state machine. I find it to be a decent way to avoid the LiveData navigation issues that gave rise to solutions like SingleLiveEvent<T>
. Otherwise, as far as I can tell, you'd need to have a chain of events and states that indicate navigation, a navigation state, a navigation consumed event, and a non-navigation state. The same might be true for analytics getting triggered again depending on the exit state. You might also look for articles about Event Sourcing with respect to Redux. I can't find it right now, but I recall one that specifically deals with wanting to trigger analytics and using Event Sourcing to have Events act as a precursor to Actions.
Although analytics tracking can be handled in a SideEffect
I think tracking can be easily handled beforeeven hitting he reducer likr this:
userActions
. doOnNext { action -> analytics.track(action) }
.reduxStore(...)
...
The same I would do for Navigation:
Either trigger navigation as part of a state transition (see mu comment above where I suggest to use .share() ) or of state doesn't change anyway do it outside of reduxStore()
im your ViewModel or Presenter. I mean i.e. clicking on a button just moves to next screen,, I think it is ok to react to this click event in ViewModel / Presenter and just navigate to next screen without even emitting this action to the reducer. Also that could be used in combination with the coordinator pattern:
One questions I have is where should I observe the database, location or socket changes? Now I see a couple of ways:
stateMachine
.sideEffect
after some initial action.@sockeqwe Which way you suggest to use or mb I overlook other option?
Definitely inside the SideEffect
. Actually I would say you hace a single SideEffect
just for observing Database, a single SideEffect
just for Location changes, a single SideEffect
just for Socket changes.
@sockeqwe Another question I have is what do you find more practical, to have several States for each kind of Error such as PaginationErrorState, SocketErrorState, LocationErrorState, RequestErrorState etc. or to have one ErrorState with a bunch of booleans?
Problems I see with approaches:
Hard to say, I think most of the time we just have a ErrorState(val message : String)
or something like that.
@sockeqwe How do you apply rxRedux on complex screens? I have several things which I'm worried about:
How do you handle back navigation when view recreated and state machine unsubscribed. So far my idea is to fire special Action to rerender the last state.
But here comes another problem that my states are incremental i.e. one state may render one part of the content and another state renders another part of the screen. This is done so to avoid excessive resetting of the same values to views and lists. But when the view is completely recreated I need all these incremental changes gathered together.
I have an idea to keep all data about the current screen(content, errors) in all states, but then it becomes a nightmare to reduce states. Especially it is unclear how to merge different kinds of errors, for example, we have errors which displayed constantly and errors which only shown temporarily and shouldn't be shown again.
How do you handle errors, for example, I have a network call in one of the side effects, but I'm getting a random NPE not because of the network down by the chain. Do you check instance of Error in onErrorReturn and crash the app there, or you do it later during reduce? In my previous redux-like architecture I was crashing the app in onErrorReturn.
@ar-g You can save your state using following delegate:
class SaveStateDelegate<T : Parcelable>(
private val bundleKey: String
) : ReadWriteProperty<Any, T?> {
private var state: T? = null
fun onSaveInstanceState(bundle: Bundle) {
bundle.putParcelable(bundleKey, state)
}
fun onRestoreInstanceState(bundle: Bundle) {
state = bundle.getParcelable(bundleKey)
}
override fun getValue(thisRef: Any, property: KProperty<*>): T? {
return state
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) {
state = value
}
}
Instance of this SaveStateDelegate
should be also provided to your flow holder (Activity
or Fragment
) and holder should call according methods from delegate.
Then, when you create redux:
private var currentState by saveStateDelegate
actions
.reduxStore(
initialState = currentState ?: InitialState(),
reducer = reducer,
sideEffects = listOf(...)
)
.distinctUntilChanged()
.doOnNext { currentState = it }
Works for us pretty good.
- How do you handle back navigation when view recreated and state machine unsubscribed. So far my idea is to fire special Action to rerender the last state.
Do you use ViewModel from Architecture Components? then this should not be an issue at all because ViewModel survive config changes and backstack navigation and keeps the subscription to state machine alife. If you really need some custom things and some way to restore state I would suggest to use the restored state as initial state for reduxStore()
and start from there (see @Tapchicoma answer above).
- But here comes another problem that my states are incremental i.e. one state may render one part of the content and another state renders another part of the screen. This is done so to avoid excessive resetting of the same values to views and lists. But when the view is completely recreated I need all these incremental changes gathered together.
I think that is not a good design choice. Make everything atomic by having state represent everything that is visible on the screen. Let the UI or ViewModel or Presenter figure out how to bring the state of the state machine most efficiently to your UI or let the UI do optimizations like if list hasn't changed then do not update the list at all.
That avoids this kind of problems from the very beginning.
- I have an idea to keep all data about the current screen(content, errors) in all states, but then it becomes a nightmare to reduce states. Especially it is unclear how to merge different kinds of errors, for example, we have errors which displayed constantly and errors which only shown temporarily and shouldn't be shown again.
So if I understood you correctly your reducer is too complex, right? Well nobody ever said you must have one single giant reducer: you can split your reducer in smaller pieces that only reduce a certain part of your state like:
fun reduce(action : Action, state: State){
return State(
someItems = reduceItems(action, state),
foo = reduceFoo(action, state),
bar = reduceBar(action, state),
error = reduceError(action, state),
...
}
private fun reduceItems(action : Action, state : State) : List<Items> =
if (action is ItemsLoadedAction){
action.loadedItems
} else {
state.someItems
}
...
At the end it's pretty much what the copy constructor from kotlin data classes does.
Also on a side note: You reducer is complex because your screen or your problem you try to solve is complex. Before it might wasn't that clear to you because you had a single point where you properly dealt with all that complexity. This complexity was spread all over your app: a bit was in your View Layer, a bit in some UI widgets like RecyclerView.Adapter, a bit in your Presenter / ViewModel, a bit in your business logic but the probelm per se is still complex. Now with a reducer you are sure that you deal with it in a proper way.
How do you handle errors, for example, I have a network call in one of the side effects, but I'm getting a random NPE not because of the network down by the chain.
Fix the random NPE first :)
Do you check instance of Error in onErrorReturn and crash the app there, or you do it later during reduce? In my previous redux-like architecture I was crashing the app in onErrorReturn.
You can override a RxJava plugin to specify there if your app should crash or not (forward it too handle to .onErrorReturn() and so on. I can't remember what the name was but similar on how you can override Schedulers i.e. for testing, you can provide your own plugin that determines if an error should be caught (and reported to onErrorReturnNext() etc.) or not. With that said, I wouldn't do that. Any network call should be caught imho because it is not a "unrecoverable" error that would lead to a unpredictable behavior of your app. So I wouldn't crash your app at all I guess, only crash if your app is unusable after that kind of error.
@sockeqwe thanks for this small library, it works fine so far.
Back to the problem with persisting state. We use presenters which survive config changes. We do subscribe/unsubscribe in onAttach/onDetach. To keep reduxStore observables we use
.publish().autoConnect(1) { disposable = it }
and when the user goes to the previous screen we fire
if (firstTime) {
stateMachine.input.accept(Action.LoadFirstPageAction)
firstTime = false
} else {
stateMachine.input.accept(Action.RenderWholeScreenAction)
}
This works fine for our case so far.
We've begun to cover our state machines with tests. There are a couple of issues:
How to organize code in unit tests, it is very repetitive, especially state recreation and assertions. For example:
val testObserver = mChatsStateMachine.state.test()
mChatsStateMachine.input.accept(Action.OnChatLongClickedAction(vms[0]))
mChatsStateMachine.input.accept(Action.DeleteChatsAction)
val state = testObserver.values()[3] as State
val contentStateWithDelete = State.ContentState(
state.items,
state.page,
state.isDeleteMode,
state.chatsToDelete,
state.errorBag
)
val requestLoadingState = State.RequestLoadingState(
state.items,
state.page,
state.isDeleteMode,
state.chatsToDelete,
state.errorBag
)
val contentStateWithoutChat = State.ContentState(
contentState.items.toMutableList().apply { removeAt(0) },
contentState.page,
contentState.isDeleteMode,
contentState.chatsToDelete,
contentState.errorBag
)
testObserver.assertValueAt(0, initialState)
testObserver.assertValueAt(1, contentLoadingState)
testObserver.assertValueAt(2, contentState)
testObserver.assertValueAt(3, contentStateWithDelete)
testObserver.assertValueAt(4, requestLoadingState)
testObserver.assertValueAt(5, contentStateWithoutChat)
A few questions:
PaginationStateMachine
which creates its own store - are you advocating creating a store for every state machine, and a state machine for every view/model?PaginationStateMachine
instead be something likeApplicationStateMachine
in order to avoid many stores?I like the Redux pattern, and I've used it for Android development in the past, but this incarnation leaves me with a few questions.