roxie
roxie copied to clipboard
Dealing with one-time effects
Here is a scenario:
Your screen supports rotation. A particular Action
can generate an error which should display a Snackbar
.
How do we deal with it? Emit the error State
initially but not after rotation to avoid displaying the Snackbar
twice? What should the State
be after rotation then?
Should we introduce a concept of Effect
s (single event type State
s)?
In the spirit of Roxie's lightweight way of doing things, it would be ideal to solve this with minimal code needed by the consumer apps.
I ran into a similar issue before with regards to State
s that navigate to another screen (IIRC the issue was we'd keep navigating to Screen B
when we back out of it to Screen A
because the same navigation State
was being rendered each time). I just went with an intermediate action/state in this case.
I think the actual fix is using something like https://github.com/googlesamples/android-architecture/blob/dev-todo-mvvm-live/todoapp/app/src/main/java/com/example/android/architecture/blueprints/todoapp/SingleLiveEvent.java#L38 - since the state won't be emitted automatically, if this is what you meant by Effect
s - I'm down for implementing something like this.
I'd say ideally we just make our observable state a SingleLiveEvent
rather than a MutableLiveData
(this is assuming there isn't a use case where we'd need to render the state without explicitly calling setValue
/postValue
:thinking:).
So there would be a LiveData
for regular State
and SingleLiveEvent
for effects? And the observing views would subscribe to both of them? And each effect would define what State
to leave the state machine in prior to emitting the effect as SingleLiveEvent
?
Jmmm, I was originally thinking of just using SingleLiveEvent
for our State
and not using (an explicit) LiveData
at all - cause I couldn't think of a case where we'd want to render a State without an explicit setValue
being called.
Thinking about it some more this probably wouldn't work, since we may have some loaded state that we always want rendered without an explicit action to load it. Something like:
- Start fragment and enter loaded state via a load action.
- Rotate device to trigger configuration change stuff or w/e.
- Just render existing loaded state rather than trigger an explicit action to load again.
So we'll probably have to go with two different
State
s or aState
and anEffect
?
The super simple solution would just have our Rx chains that we want to act as SingleLiveEvent
always emit another intermediate State
after it's real state.
useCase.loadStuff()
.toObservable()
.map<Change> { Change.Load(it) }
.onErrorReturn { error -> Change.Error(error) }
// Intermediate state here
.flatMap { Observable.just<Change>(Change.Idle) }
.subscribeOn(Schedulers.io())
.startWith(Change.Loading)
When onActivityCreated
or whatever gets called from configuration change or fragment replacement (I've run into this issue with the back button and the android navigation component because it doesn't support add
for fragment transactions :angry:) and we observe the VM's state
we just render Idle
rather than the last state
(which could be Error
or Loaded
, would just need to move some code around to have it only apply to the Error
change).
I've just run into the same problem when trying to use the Navigation Component. I was thinking along the same lines as you by introducing another emitter using SingleLiveEvent.
That way you have States which change the state of the UI and you have Events/Effects that trigger something like navigation, snackbars, dialogs, permission requests etc
The question is how to split Changes into either States or Events, an example would be the user pressing a "locate me" button, the Action triggers a Change that updates the State to show a progress spinner, whilst the Event needs to trigger a permission request. It gets trickier when we need to do something based on the results of the Event, did they accept it or did they reject it etc
@Glurt Just FYI. Trying to relay navigation update as event/effect (SingleLiveEvent) fails in an edge case with lifecycle and fragment transactions.
https://github.com/kaushikgopal/movies-usf/issues/19
This seems like a good way to handle single events https://ryanharter.com/blog/handling-transient-events/ events can be dispatched from within bindActions()
This seems like a good way to handle single events https://ryanharter.com/blog/handling-transient-events/ events can be dispatched from within bindActions()
Checked it, the subscription until onDestroy still has the same problem while the app is backgrounded and the event notification (from PublishRelay) tries to perform a fragment transaction on a paused app.