spotify/mobius

Confused about Android configuration changes with requests in flight

nihk opened this issue · 14 comments

nihk commented

Hi. Let me preface this question by saying I'm still learning this library as well as RxJava.

My question is: say you've got an effect that performs some fake work on a separate thread, e.g.

fun doFakeWork(work: Observable<DoWorkEffect>): Observable<Event> {
    return work
        .delay(5, TimeUnit.SECONDS)
        .map { DoneLoading }
}

and that's hooked up neatly into an effects handler. And say this effect was a consequence of an event that puts the model into a loading state. If the user rotates their phone and destroys the hosting activity during this loading state, the model's state can be saved and restored via the savedInstanceState bundle -- great. But I'm noticing I have nothing to restore and continue the observable stream from where it left off, meaning the fake work being done in that above effect doesn't go beyond the delay operator, and thus the DoneLoading event won't fire to clear the model of its loading state.

How should I be better handling this scenario? I thought to put the effects handler and loop factory into an AAC ViewModel, but the observable stream still ceases upon rotation. I'm not entirely sure why, but I'm guessing it's because the controller still needs to be in an activity/fragment (to bind to the view events, e.g. clicks) and the stream is only subscribed to in that class.

You are correct @nihk. When you stop the Controller, it'll dispose of the MobiusLoop, which will dispose of all effect handlers. Disposing of effect handlers will lead to the teardown of any subscriptions that were made by these effect handlers. That's why you're not seeing DoneLoading when you rotate the device. Typically I handle these cases by dispatching the effect again from my Init function when I see that I was in a state that implies that I was doing something. For example, if I had an isLoading flag on my model that was set to true, that is an indication that I should be dispatching a LoadData effect to fetch data again. If I have side-effects that are more expensive, I'll typically have them run in a non-UI android but that might be overkill for some apps.

The behavior really comes from RxJava. You'd still have that behavior if you weren't using Mobius. Disposing of a subscription to an RxJava observable will cancel anything that observable was doing unless you apply certain operators that prevent that from happening (eg. .cache() will keep the observable running until it completes and will replay all emissions to any new subscribers. It'll also never subscribe to the upstream again once it completes, so it's probably not what you want).

So what you need to achieve this is to keep the observable alive, get its latest emission when the activity is recreated and only dispose of it when the component is being destroyed permanently. I think perhaps applying autoConnect and managing that "connection" and disposing of it when the activity/fragment is being destroyed. That thing keeping the observable alive should probably live in an AAC ViewModel, and should be a dependency of your effect handler. So your effect handler would change a bit cuz you'd need to merge your workToEvent stream with the previousWork stream coming from that component.

These are just my initial thoughts. I'll experiment a bit with the idea and post back my findings :)

nihk commented

Thanks @anawara. That was a really helpful and informative response. I think I was trying to achieve what you had described -- keeping the observable alive in the ViewModel -- but I'm still trying to get familiar with the Mobius API in order to do that in a satisfactory way. I'll keep at it to see if I can get that going with your suggestions considered. I look forward to any further findings you have for that, as well! Thanks again.

I am also interested in a recipe on how to run a task to completion from an effect handler even though the mobius loop has been stopped. If the loop has not been stopped, I still want to receive events from that task and if the loop stopped, I would want the task to run to completion nonetheless.

@kaciula I think it depends on the case. An AAC ViewModel won't necessarily achieve that for you. If you always want your task to run to completion, even if your container is being destroyed, then I'd suggest you run it in a background component such as an IntentService, or a foregrounded Service (that's really the only way you can guarantee that you won't be killed by the OS).

I think a ViewModel will only keep your operation alive as long as the container is being torn down for configuration changes. That means if the user presses back, then any operations you may have started would be canceled. If you take image uploading for example. To allow a user to pick a few images for upload, and then navigate through the app while the upload happens, you'd need to offload that upload operation to a non-UI container to run. And you'd then observe the state of that operation in the UI and render it. I'm not suggesting all your backend calls go through a Service of course. Just that it really depends on the case you're looking at

Yes. I understand that I have to offload the operation (to a Service or thread). My dilemma was how to feed the UI mobius loop (if it is still running) with events from that operation. One idea would be to create an event source for that particular operation and the Service to post events in the event source. Are there other approaches?

PS: I'm not using AAC ViewModel because I use Conductor and configuration changes are not an issue.

@kaciula I haven't considered other solutions as that has worked well for every scenario I have come across so far. I would be interested to hear what @anawara thinks though.

For a simple app, I made my base Conductor Controller an EventSource to push through common events that the Controller's loop may need to know about. This has the added benefit of allowing each controller to Compose their own EventSources.

For example the app allows users to initiate large file uploads from a Controller by dispatching an Effect when a button press Event occurs. The actual upload is started and handled in the background by a manager class as you would expect, it also exposes an upload state event stream that can be observed.

When a Controller is interested in the status of any uploads (most screens show the progress), I simply override the EventSource#subscribe function of the interested Controller and subscribe to the updates.

Here is a very basic example from a controller that displays upload status:

  override fun subscribe(eventConsumer: Consumer<AppEvent>): Disposable {
    val job = Job()
    val uploadManager = instance<UploadManager>()

    launch(job + UI) {
      uploadManager
          .uploadStatus
          .mapToAppEvent()
          .consumeEach(eventConsumer::accept)
    }

    return CompositeDisposable.from(
        super.subscribe(eventConsumer),
        Disposable { job.cancel() }
    )
  }

@kaciula I don't know much about Conductor but it seems like your approach would make sense in this case.

I've been in situations where the UI was to update some background state, eg. a music player, which was handled by a mobius loop in a service. So I setup the service's model changes as an event source for the UI loop and made the UI effect handler just send events to the service's mobius loop. So it kinda looked like
UI Effect -> Service Event -> Service Model Update -> UI Event -> UI Model Update

The only thing I can think of right now is if you start some operation from an effect handler, that you might want to cancel when the UI is recreated. Then the EventSource route might now work right away and you'd need to make some changes. I can think of a couple of options:

  1. Have some sort of stateful API that outlives the effect handler, and that gets injected into your effect handler. The handler can then add itself as a "listener" to that stateful API, and that API could replay whatever it would like to.

  2. Have an integer on your model that you increment every time you dispatch an effect and pack that into the effect. Then have the thing generating the resulting event pack that same value in the event. Now you can compare the integer in the event to the one on your model and decide whether or not this event is still relevant.

I prefer the latter because it helps me make sure I handle my events correctly since there's no guarantee what order the might arrive in. For example, if you dispatch F1 then F2 later and that supposedly cancels F1 (eg using switchMap). You can still receive result E1 is because it could have been already in your event queue. You could probably also use a combination of both if you need.

Please do tell me if you think of anything else!

@DrewCarlson that sounds reasonable :) I'm guessing you don't use RxJava?

Thanks @anawara and @DrewCarlson . I got some valuable ideas on how to implement this.

On a side note, is there a sample app which uses a global Mobius loop for the entire app? The only sample I know of is mobius-android-sample.

Thanks for the insight @anawara. You're correct, my usage of Mobius does not include RxJava or the rx2 companion library.

@kaciula I don't typically go with a global mobius loop for the entire app. Instead I split my app into a mobius loop per feature, and then possibly a mobius loop in a service that manages the background state, for example a music player etc. If I need something in the UI to render whatever is happening in the background loop, I'll use that background loop as an event source for the UI loop.

I have thought of how things would look if I used just a single loop with a model that represented the state of the entire application but I feel like this would result in a very complicated loop. I think probably looking into how it is done in Elm would give you more ideas about how you can do it in Android if you're interested

@anawara Yes. My question was actually about having a loop for all the background things happening in the app and notifying the current screen about it. I agree that a single model for the entire app is not feasible.

Final question! :) Could you please explain how you are able to implement "background loop as an event source for the UI loop"? How do you hook up the background loop model changes to the UI loop as an event source? Is there something in the library that allows me to do this?

Thank you for your patience and answers.

togi commented

In the case that @anawara is referring to the background loop lives in an android Service, and the EventSource is hooked up through a service binding. There's nothing specifically for this in the library right now, but you could definitely imagine creating something like a ServiceBindingEventSource and putting it in the mobius-android module. Contributions are welcome :)

The original question is solved by MobiusLoopViewModel, so closing this issue.