It's October 2017, and Google makes Android Support Library with the new things call Lifecycle component. It provides some new idea for this 'Can not perform this action after onSaveInstanceState' problem.
In short:
Longer version with explain:
why this problem come out?
It's because you are trying to use FragmentManager
from your activity(which is going to hold your fragment I suppose?) to commit a transaction for you fragment. Usually this would look like you are trying to do some transaction for an up coming fragment, meanwhile the host activity already call savedInstanceState
method(user may happen to touch the home button so the activity calls onStop()
, in my case it's the reason)
Usually this problem shouldn't happen -- we always try to load fragment into activity at the very beginning, like the onCreate()
method is a perfect place for this. But sometimes this do happen, especially when you can't decide what fragment you will load to that activity, or you are trying to load fragment from an AsyncTask
block(or anything will take a little time). The time, before the fragment transaction really happens, but after the activity's onCreate()
method, user can do anything. If user press the home button, which triggers the activity's onSavedInstanceState()
method, there would be a can not perform this action
crash.
If anyone want to see deeper in this issue, I suggest them to take a look at this blog post. It looks deep inside the source code layer and explain a lot about it. Also, it gives the reason that you shouldn't use the commitAllowingStateLoss()
method to workaround this crash(trust me it offers nothing good for your code)
How to fix this?
Should I use commitAllowingStateLoss()
method to load fragment? Nope you shouldn't;
Should I override onSaveInstanceState
method, ignore super
method inside it? Nope you shouldn't;
Should I use the magical isFinishing
inside activity, to check if the host activity is at the right moment for fragment transaction? Yeah this looks like the right way to do.
Take a look at what Lifecycle component can do.
Basically, Google makes some implementation inside the AppCompatActivity
class(and several other base class you should use in your project), which makes it a easier to determine current lifecycle state. Take a look back to our problem: why would this problem happen? It's because we do something at the wrong timing. So we try not to do it, and this problem will be gone.
I code a little for my own project, here is what I do using LifeCycle
. I code in Kotlin.
val hostActivity: AppCompatActivity? = null // the activity to host fragments. It's value should be properly initialized.
fun dispatchFragment(frag: Fragment) {
hostActivity?.let {
if(it.lifecyclecurrentState.isAtLeast(Lifecycle.State.RESUMED)){
showFragment(frag)
}
}
}
private fun showFragment(frag: Fragment) {
hostActivity?.let {
Transaction.begin(it, R.id.frag_container)
.show(frag)
.commit()
}
As I show above. I will check the lifecycle state of the host activity. With Lifecycle component within support library, this could be more specific. The code lifecyclecurrentState.isAtLeast(Lifecycle.State.RESUMED)
means, if current state is at least onResume
, not later than it? Which makes sure my method won't be execute during some other life state(like onStop
).
Is it all done?
Of course not. The code I have shown tells some new way to prevent application from crashing. But if it do go to the state of onStop
, that line of code wont do things and thus show nothing on your screen. When users come back to the application, they will see an empty screen, that's the empty host activity showing no fragments at all. It's bad experience(yeah a little bit better than a crash).
So here I wish there could be something nicer: app won't crash if it comes to life state later than onResume
, the transaction method is life state aware; besides, the activity will try continue to finished that fragment transaction action, after the user come back to our app.
I add something more to this method:
class FragmentDispatcher(_host: FragmentActivity) : LifecycleObserver {
private val hostActivity: FragmentActivity? = _host
private val lifeCycle: Lifecycle? = _host.lifecycle
private val profilePendingList = mutableListOf<BaseFragment>()
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun resume() {
if (profilePendingList.isNotEmpty()) {
showFragment(profilePendingList.last())
}
}
fun dispatcherFragment(frag: BaseFragment) {
if (lifeCycle?.currentState?.isAtLeast(Lifecycle.State.RESUMED) == true) {
showFragment(frag)
} else {
profilePendingList.clear()
profilePendingList.add(frag)
}
}
private fun showFragment(frag: BaseFragment) {
hostActivity?.let {
Transaction.begin(it, R.id.frag_container)
.show(frag)
.commit()
}
}
}
I maintain a list inside this dispatcher
class, to store those fragment don't have chance to finish the transaction action. And when user come back from home screen and found there is still fragment waiting to be launched, it will go to the resume()
method under the @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
annotation. Now I think it should be working like I expected.