Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nested Fragments - IllegalStateException "Can not perform this action after onSaveInstanceState"

Background

Asynchronous Callbacks in Android

Trying to perform an asynchronous operation in a reliable fashion on Android is unnecessarily convoluted i.e. Is AsyncTask really conceptually flawed or am I just missing something?

Now, this is all prior to the introduction of Fragments. With the introduction of Fragments, onRetainNonConfigurationInstance() has been deprecated. So the latest Google condoned hack is to use a persistent non-UI fragment that attaches/detaches from your Activity when configuration changes occur (i.e. Rotating the screen, changing language settings etc.)

Example: https://code.google.com/p/android/issues/detail?id=23096#c4

IllegalStateException - Can not perform this action after onSaveInstanceState

Theoretically the hack above allows you to get around the dreaded:

IllegalStateException - "Can not perform this action after onSaveInstanceState"

because a persistent non-UI fragment will receive callbacks for onViewStateRestored() (alternatively onResume) and onSaveInstanceState() (alternatively onPause). As such you can tell when the instance state is saved/restored. It's a fair bit of code for something so simple, but utilising this knowledge, you could queue up your asynchronous callbacks until the activity's FragmentManager has its mStateSaved variable set to false before executing them.

mStateSaved being the variable who is ultimately responsible for firing this exception.

private void checkStateLoss() {
    if (mStateSaved) {
        throw new IllegalStateException(
                "Can not perform this action after onSaveInstanceState");
    }
    if (mNoTransactionsBecause != null) {
        throw new IllegalStateException(
                "Can not perform this action inside of " + mNoTransactionsBecause);
    }
}

So theoretically, now you know when it's safe to perform fragment transactions, and you can hence avoid the dreaded IllegalStateException.

Wrong!

Nested Fragments

The solution above only works for the Activity's FragmentManager. Fragments themselves also have a child fragment manager, which is utilised for nesting fragments. Unfortunately, child fragment managers are not kept in sync with the Activity's fragment manager at all. So whilst the activity's fragment manager is up-to-date and always has the correct mStateSaved; child fragments think otherwise and will happily throw the dreaded IllegalStateException at inappropriate times.

Now, if you've looked at Fragment.java and FragmentManager.java in the support library you won't in any way be surprised that everything and anything to do with fragments is error prone. The design and code quality is... ah, questionable. Do you like booleans?

mHasMenu = false
mHidden = false
mInLayout = false
mIndex = 1
mFromLayout = false
mFragmentId = 0
mLoadersStarted = true
mMenuVisible = true
mNextAnim = 0
mDetached = false
mRemoving = false
mRestored = false
mResumed = true
mRetainInstance = true
mRetaining = false
mDeferStart = false
mContainerId = 0
mState = 5
mStateAfterAnimating = 0
mCheckedForLoaderManager = true
mCalled = true
mTargetIndex = -1
mTargetRequestCode = 0
mUserVisibleHint = true
mBackStackNesting = 0
mAdded = true

Anyway, getting a bit off topic.

Dead-end solution

So, you might think the solution to the problem is simply, which seems like a bit of an antonym at this point, to add another one of those nice hacky non-UI fragments to your child fragment managers. Presumably its callbacks are in sync with its internal state and things will all be dandy.

Wrong again!

Android doesn't support retained fragment instances that are attached as children to other fragments (aka nested fragments). Now, in hindsight this should make sense. If the parent fragment is destroyed when the activity is destroyed because its not retained, there is nowhere to reattach the child fragment. So that's just not going to work.

My Question

Does someone out there have a solution for determining when it is safe to perform fragment transactions on child fragments in conjunction with asynchronous code callbacks?

like image 299
Benjamin Dobell Avatar asked Jul 25 '13 02:07

Benjamin Dobell


1 Answers

Update 2

React Native

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!

Can't or Won't use React Native

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:

  1. Move to simple message passing system based around 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.
  2. Use a Reactive event library (e.g. RxJava). I've not tried this myself on Android, but had pretty good success using a similar concept library, ReactiveCocoa for a Mac/desktop app. Admittedly these libraries have a fairly steep learning curve, but the approach is quite refreshing once you get used to it.

Update 1: Quick and Dirty (Official) Solution

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.

My Solution

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").

Conclusion

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.

like image 93
Benjamin Dobell Avatar answered Oct 21 '22 17:10

Benjamin Dobell