ww-tech/roxie

Dealing with one-time effects

jshvarts opened this issue ยท 7 comments

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 Effects (single event type States)?

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 States 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 Effects - 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 ๐Ÿค”).

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:

  1. Start fragment and enter loaded state via a load action.
  2. Rotate device to trigger configuration change stuff or w/e.
  3. Just render existing loaded state rather than trigger an explicit action to load again.
    So we'll probably have to go with two different States or a State and an Effect?

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 ๐Ÿ˜ ) 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.

kaushikgopal/movies-usf-android#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.