Trying to perform an asynchronous operation in a reliable fashion on Android is unnecessarily convoluted i.e. Is
If you can stomach it, use React Native. I know, I know... "dirty web technologies", but in all seriousness, the Android SDK is a disaster, so swallow your pride and just give it a go. You might surprise yourself; I know I did!
No worries, I'd suggest fundamentally changing your approach to networking. Firing a request and running a request handler to update the UI just doesn't work well with Android's component life-cycles.
Instead try one of:
LocalBroadcastReceiver
and have long-living objects (regular Java classes or Android Services) do your requests and fire events when your app's local state changes. Then in your Activity/Fragment, just listen for certain Intent
and update accordingly.I believe this is latest official solution from Google. However, the solution really doesn't scale very well. If you're not comfortable messing with queues, handlers and retained instance states yourself then this may be your only option... but don't say I didn't warn you!
Android activities and fragments have support for a LoaderManager which can be used with AsyncTaskLoader. Behind the scenes loader managers are retained in precisely the same way as retained fragments. As such this solution does share a bit in common with my own solution below. AsyncTaskLoader is a partially pre-canned solution that does technically work. However, the API is extremely cumbersome; as I'm sure you'll notice within a few minutes of using it.
Firstly, my solution is by no means simple to implement. However, once you get your implementation working it's a breeze to use and you can customise it to your heart's content.
I use a retained fragment that is added to the Activity's fragment manager (or in my case support fragment manager). This is the same technique mentioned in my question. This fragment acts as a provider of sorts which keeps track of which activity it is attached to, and has Message and Runnable (actually a custom sub-class) queues. The queues will execute when the instance state is no longer saved and the corresponding handler (or runnable) is "ready to execute".
Each handler/runnable stores a UUID that refers to a consumer. Consumers are typically fragments (which can be nested safely) somewhere within the activity. When a consumer fragment is attached to an activity it looks for a provider fragment and registers itself using its UUID.
It is important that you use some sort of abstraction, like UUID, instead of referencing consumers (i.e. fragments) directly. This is because fragments are destroyed and recreated often, and you want your callbacks to have a "reference" to new fragments; not old ones that belong to a destroyed activity. As such, unfortunately, you rarely can safely use variables captured by anonymous classes. Again, this is because these variables might refer to an old destroyed fragment or activity. Instead you must ask the provider for the consumer that matches the UUID the handler has stored. You can then cast this consumer to whatever fragment/object it actually is and use it safely, as you know its the latest fragment with a valid Context (the activity).
A handler (or runnable) will be "ready to execute" when the consumer (referred to by UUID) is ready. It is necessary to check if the consumer is ready in addition to the provider because as mentioned in my question, the consumer fragment might believe its instance state is saved even though the provider says otherwise. If the consumer (or provider) are not ready then you put the Message (or runnable) in a queue in the provider.
When a consumer fragment reaches onResume() it informs the provider that it is ready to consume queued messages/runnables. At which point the provider can try execute anything in its queues that belong to the consumer that just became ready.
This results in handlers always executing using a valid Context (the Activity referenced by the provider) and the latest valid Fragment (aka "consumer").
The solution is quite convoluted, however it does work flawlessly once you work out how to implement it. If someone comes up with a simpler solution then I'd be happy to hear it.