I thought that when I call
fragment.show(getSupportFragmentManager(), tag);
the activity were to PAUSED state by the calling of the onPause() method.
But debugging the project I discovered that no lifecycle method is called.
I was planning the flow to use the onPause() and onResume() methods when I call and dismiss a fragment, but it is confusing me. Can someone help me please?
Some observations regarding the show() method:
It belongs to a DialogFragment (link);
It is only described by a fragment transaction (does not affect activity into enter onPause() state):
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit();
}
Detailed information about an Activity lifecycle: link1 and link2
Related
I am developing an application with fragments. It has a JavaScript Interface, which is called in the Main Activity and has fragment replacing logic. When application is in foreground everything works OK, but when the application is in background, fragment transaction replace doesn't work. When I return to my application, I still see the old fragment and don't see the new one.
#JavascriptInterface
public void beginCall(String toast) {
FragmentTransaction fTrans;
taskFragment = TaskFragment.newInstance(toast,"");
fTrans = getSupportFragmentManager().beginTransaction();
fTrans.replace(R.id.frgmCont, taskFragment);
fTrans.commit();
}
What is wrong? Why the fragment transaction doesn't work in background?
After some time I've found the answer: it's impossible to perform a fragment transaction after onStop, it will result in java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState. I wasn't getting that Exception as JavascriptInterface was performed in a separate thread. When I forced my code to run in Main thread, I got that error. So I need to implement a different logic, also using some of Activity Life-cycle methods, or to switch to multiple activities logic. Hope my answer will help anyone.
Some use cases or architectures might require to trigger fragment transactions while app is in background.
We created following extension function:
fun FragmentTransaction.commitWhenStarted(lifecycle: Lifecycle) {
lifecycle.addObserver(object : LifecycleObserver {
#OnLifecycleEvent(value = Lifecycle.Event.ON_START)
fun onStart() {
lifecycle.removeObserver(this)
commit()
}
})
}
Use it just like any other version of commit, commitNow, commitAllowingStateLoss.
If the activity state is already at least started the observer will be called directly and the fragment transaction is executed. The lifecycle can be taken from activity or from fragment if the transaction is executed on a childFragmentManager
transaction.commitWhenStarted(lifecycle)
FragRecordSongList FragRecordSongList = new FragRecordSongList();
FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
ft.addToBackStack(FragRecordSongList.class.getName());
ft.replace(R.id.fragContainer, FragRecordSongList, FragRecordSongList.class.getName());
ft.commit();
Try this may be help you
#lilienberg commented a great solution for fragment transactions. If you are using the navigation component you can use something like this:
fun NavController.resumedNavigation(lifecycle: Lifecycle, destination: Int) {
if(lifecycle.currentState.isAtleast(Lifecycle.State.RESUMED)){
//App is resumed, continue navigation.
navigate(destination)
} else {
//When app is resumed, remove observer and navigate to destination/
lifecycle.addObserver(object: LifecycleObserver {
#OnLifecycleEvent(value = Lifecycle.Event.ON_RESUME)
fun onResume() {
lifecycle.removeObserver(this)
navigate(destination)
}
})
}
}
You can call this function from your Activity or Fragment like this:
findNavController(R.id.my_nav_host_fragment).resumedNavigation(
lifecycle, R.id.my_navigation_action)
I have got exception when ft.commit() and I don't know why.
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1448)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1466)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:634)
at android.support.v4.app.BackStackRecord.commit(BackStackRecord.java:613)
at MainActivity.attachFragment(MainActivity.java:242)
at MainActivity.attachFragment(MainActivity.java:225)
at MainActivity.showHome(MainActivity.java:171)
at MainActivity.onComplete(MainActivity.java:278)
at MDownloadManager.onDownloadComplete(MDownloadManager.java:83)
at DownloadRequestQueue$CallBackDelivery$2.run(DownloadRequestQueue.java:61)
at android.os.Handler.handleCallback(Handler.java:733)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:149)
at android.app.ActivityThread.main(ActivityThread.java:5257)
at java.lang.reflect.Method.invokeNative(Method.java)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:609)
at dalvik.system.NativeStart.main(NativeStart.java)
Here is my method where crash is comming.
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
if(addToBackStack) {
ft.addToBackStack(null);
ft.add(R.id.frame_container, fragment, tag);
} else {
ft.replace(R.id.frame_container, fragment, tag);
}
ft.commit();
Have you got any idea what is wrong?
I don't use onSaveInstanceState in my project.
Complete solution at Solution for IllegalStateException
Overriding onSaveInstanceSate is a hack which doesnt necessarily work for all the scenerios. Also using commitAllowingStateLoss() is dangerous and could lead to UI irregularities.
We need to understand that IllegalStateException is encountered when we try to commit a fragment after the Activity state is lost - Activity is not in foreground (to understand more about Activity states read this). Therefore to avoid (resolve) this exception we just delay our fragment transaction until the state is restored
Declare two private boolean variables
public class MainActivity extends AppCompatActivity {
//Boolean variable to mark if the transaction is safe
private boolean isTransactionSafe;
//Boolean variable to mark if there is any transaction pending
private boolean isTransactionPending;
Now in onPostResume() and onPause we set and unset our boolean variable isTransactionSafe. Idea is to mark trasnsaction safe only when the activity is in foreground so there is no chance of stateloss.
/*
onPostResume is called only when the activity's state is completely restored. In this we will
set our boolean variable to true. Indicating that transaction is safe now
*/
public void onPostResume(){
super.onPostResume();
isTransactionSafe=true;
}
/*
onPause is called just before the activity moves to background and also before onSaveInstanceState. In this
we will mark the transaction as unsafe
*/
public void onPause(){
super.onPause();
isTransactionSafe=false;
}
private void commitFragment(){
if(isTransactionSafe) {
MyFragment myFragment = new MyFragment();
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.frame, myFragment);
fragmentTransaction.commit();
}
}
What we have done so far will save from IllegalStateException but our transactions will be lost if they are done after the activity moves to background, kind of like commitAllowStateloss(). To help with that we have isTransactionPending boolean variable
public void onPostResume(){
super.onPostResume();
isTransactionSafe=true;
/* Here after the activity is restored we check if there is any transaction pending from
the last restoration
*/
if (isTransactionPending) {
commitFragment();
}
}
private void commitFragment(){
if(isTransactionSafe) {
MyFragment myFragment = new MyFragment();
FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.frame, myFragment);
fragmentTransaction.commit();
isTransactionPending=false;
}else {
/*
If any transaction is not done because the activity is in background. We set the
isTransactionPending variable to true so that we can pick this up when we come back to
foreground
*/
isTransactionPending=true;
}
}
The onSaveInstanceState method is part of the activity lifecycle. So, even if you don't call it explicitly, it is call at some point by your Activity.
So the question is where in the activity lifecycle did you use the code you show us ?
One workaround is to use commitAllowingStateLoss instead of commit for the fragment transaction.
(You should read the description in the link to see if it is ok for you to use this method)
I had the same issue but I was able to solve this by overriding onSaveInstanceState and comment the line of calling its super like this in fragment.
#Override
public void onSaveInstanceState(Bundle outState) {
// super.onSaveInstanceState(outState);
}
Hope that help.
Here is an updated solution using Kotlin. For full details you can check this article : Avoid Fragment IllegalStateException: Can not perform this action after onSaveInstanceState
class MainActivity : AppCompatActivity(){
private var isActivityResumed = false
private var lastCall: (() -> Unit)? = null
companion object {
private const val ROOT_FRAGMENT = "root"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//Call some expensive async operation that will result in onRequestCallback below
myExpensiveAsyncOperation()
}
override fun onPause() {
super.onPause()
//Very important flag
isActivityResumed = false
}
override fun onResume() {
super.onResume()
isActivityResumed = true
//If we have some fragment to show do it now then clear the queue
if(lastCall != null){
updateView(lastCall!!)
lastCall = null
}
}
/**
* Fragment Management
*/
private val fragmentA : () -> Unit = {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer_fl, FragmentA())
.addToBackStack(ROOT_FRAGMENT)
.commit()
}
private val fragmentB : () -> Unit = {
supportFragmentManager.beginTransaction()
.replace(R.id.fragmentContainer_fl, FragmentB())
.addToBackStack(ROOT_FRAGMENT)
.commit()
}
private val popToRoot : () -> Unit = { supportFragmentManager.popBackStack(ROOT_FRAGMENT,0) }
// The function responsible for all our transactions
private fun updateView(action: () -> Unit){
//If the activity is in background we register the transaction
if(!isActivityResumed){
lastCall = action
} else {
//Else we just invoke it
action.invoke()
}
}
// Just an example
private fun onRequestCallback() {
if(something) {
updateView(fragmentA)
else {
updateView(fragmentB)
}
}
It's pretty simple, you cannot commit fragment transactions in an activity after onSaveInstanceState(Bundle outState) has been called. When is your code being called?
onSavedInstanceState is called as part of the activity lifecycle when a configuration change occurs. You have no control over it.
Hope this will help
EDIT1: after some more research, this is a known bug in the support package.
If you need to save the instance, and add something to your outState Bundle you can use the following :
#Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString("WORKAROUND_FOR_BUG_19917_KEY", "WORKAROUND_FOR_BUG_19917_VALUE");
super.onSaveInstanceState(outState);
}
EDIT2: this may also occur if you are trying to perform a transaction after your Activity is gone in background. To avoid this you should use commitAllowingStateLoss()
EDIT3: The above solutions were fixing issues in the early support.v4 libraries from what I can remember. But if you still have issues with this you MUST also read #AlexLockwood 's blog : Fragment Transactions & Activity State Loss
Summary from the blog post (but I strongly recommend you to read it) :
NEVER commit() transactions after onPause() on pre-Honeycomb, and onStop() on post-Honeycomb
Be careful when committing transactions inside Activity lifecycle methods. Use onCreate(), onResumeFragments() and onPostResume()
Avoid performing transactions inside asynchronous callback methods
Use commitAllowingStateLoss() only as a last resort
If you are using coroutines in your project you can easily make sure that your code will runs when lifecycle state is at least Started and not destroyed.
lifecycleScope.launchWhenStarted{}
I have an IllegalStateException on showing a DialogFragment :
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
i know why its happening but i want to using commitAllowingStateLoss on showing dialog by overriding DialogFragment show function :
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit(); //replace it by commitAllowingStateLoss
}
but i don't access to mDismissed and mShownByMe variables , how can i access those variables to modify them as it's parent did.
I think to prevent throwing IllegalStateException on DialogFragment might be better to use :
YourDialogFragment dialogFragment = new YourDialogFragment();
fragmentManager.beginTransaction().add(dialogFragment, YourDialogFragment.TAG_FRAGMENT).commitAllowingStateLoss();
instead of using show() on DialogFragment.
The solution about commitAllowingStateLoss works if your DialogFragment has no state to save, otherwise they will be lost like the function name told. But I think in most cases we have state to save, that is the major benefit of DialogFragment: Android recreate it and maintains its state automatically.
A better solution would be to check if the recreate process done, if not then return to caller, which is either an Activity or a FragmentActivity, it should call mark it and call the show function again later in its onPostResume() or onResumeFragments() callback, which we can make sure all fragments are recreated.
Here is an overridden show() from a subclass of DialogFragment:
public boolean show(FragmentManager fragmentManager) {
if (fragmentManager.isStateSaved()) return false;
show(fragmentManager, tagName);
return true;
}
Origin dialog fragment
public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commit(); //replace it by commitAllowingStateLoss
}
I don't know mDismissed, mShownByMe variables used for, so should be better if override show(FragmentManager, String) method of DialogFragment and it works fine with me
override fun show(manager: FragmentManager?, tag: String?) {
if (manager?.isDestroyed == false && !manager.isStateSaved) {
super.show(manager, tag)
}
}
isStateSaved available from appcompat >= 26.0.0 or androidx
I have ActivityA attaching FragmentA. There's an EditText in FragmentA which, if focused, adds FragmentB (below). The stack trace starts with onDestroy in ActivityA, which triggers onFocusChange, which fires off popBackStack. The isRemovingOrPartOfRemovalChain() should be returning true at this point but it occasionally returns false causing the popBackStack, hence the exception. Is there a bug in that method?
editText.setOnFocusChangeListener(new OnFocusChangeListener(){
#Override
public void onFocusChange(View view, boolean hasFocus) {
if(hasFocus){
FragmentManager fragmentManager = getChildFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(FRAGMENT_B);
if(fragment == null){
FragmentB fragmentB = FragmentB.newInstance();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.fragment_b, fragmentB, FRAGMENT_B);
fragmentExploreSearchListTransaction.addToBackStack(null);
fragmentExploreSearchListTransaction.commit();
}
else{
if(!isRemovingOrPartOfRemovalChain()){
getChildFragmentManager().popBackStack();
}
}
}
});
public boolean isRemovingOrPartOfRemovalChain(){
if(isRemoving()){
return true;
}
Fragment fragment = this.getParentFragment();
if(fragment != null){
if(((MainFragment) fragment).isRemovingOrPartOfRemovalChain()){
return true;
}
else{
return false;
}
}
else{
return(getActivity().isFinishing());
}
}
/**
* Return true if this fragment is currently being removed from its
* activity. This is <em>not</em> whether its activity is finishing, but
* rather whether it is in the process of being removed from its activity.
*/
final public boolean isRemoving() {
return mRemoving;
}
When you commit fragment after onSavedInstanceState(Bundle outState) callback (e.g. onDestroy()), the committable Fragment state will be lost (becase Fragment.onSaveInstanceState(Bundle) won't be called in this situation).
In this case, when Activity is recreated, the committed fragment will not be present in Activity's FragmentManager, and so will not be restored. This is considered a state loss.
This behaviour might break or corrupt user experience, and is considered unintentional and exceptional, so the Android framework warns you about that by throwing an exception :-) Better save than sorry, right?
In case one knows what one's doing (which is almost always not so :-)), one may stick with .commitAllowingStateLoss(), but I strongly advise against it, as it will bring a legal bughole into your application.
Just do not commit fragments after it has became known that your Activity is destroying: for example,
...
boolean fieldActivityIsDestroying;
....
public void onSaveInstanceState(Bundle out){
super.onSaveInstanceState(out);
fieldActivityIsDestroying = true;
}
and check for field value when commiting the fragment.
Also you might want to FragmentManager.executePendingTransactions() to perform any fragment transactions immediately after you commit them to manager (default commit is asynchronous).
your issue because of activity state lost.
try this
fragmentExploreSearchListTransaction.commit()
to
fragmentExploreSearchListTransaction.commitAllowingStateLoss()
but it is not good solution, so i refer you read this blog, this blog is about fragment Transaction after save Activity Instance, I hope my information will help you.
There is not enough code to provide with the complete solution, but in this case:
It's better use Fragment#isRemoving() to check if fragment removed from activity;
If focus changed expected from user interaction with the screen it's better set/remove listener at following methods:
onCreateView()/onDestroyView();
onResume()/onPause();
If there is any reason that this solutions not work feel free to clarify.
Good luck!
Why don't you just set the onFocusedChangeListener in OnResume() and remove it in OnPause()?
That should prevent it from being triggered when your activity is finishing.
the docs on setRetainInstance say :
This can only be used with fragments not in the back stack.
so I started playing with it.
I have one Activity with adds first frag A
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.content, new PackageFragment());
ft.commit
then from this frag I run a method from parent Activity which adds frag B to backstack
FragmentManager fm = getSupportFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.content, new OrderFragment());
ft.addToBackStack(null);
ft.commit();
then I create log msg from onCreate,onDestroy,onSaveInstanceState,onActivityCreated...etc
I try two versions of this process. Rotating the device on each fragment.
default
Everything is as expected. onCreate, onDestroy on fragments fire
setRetainInstance(true)
Everything is as expected?. onCreate, onDestroy on fragments dont fire
and all seems to work while fragments are in the backstack.. so why the docs say I shouldnt use it?
What are the scenarios where I might get in trouble?
thanks
Updated answer:
What are the scenarios where I might get in trouble?
When adding a Fragment to the back stack and passing a Bundle in the Fragment from onSaveInstanceState() to onCreateView() on configuration change. Calling setRetainInstance(true) will set the Bundle to null on configuration change.
(I'm not sure a developer would actually attempt this since using setRetainInstance(true) makes onSaveInstanceState() kind of redundant, but I didn't see the behaviour documented in the API docs so I wrote up this answer).
If both addToBackStack() and setRetainInstance(true) are called, setRetainInstance() partly alters the Fragment lifecycle method calls and parameter values on configuration changes, compared to calling only addToBackStack().
Specifically, in the test below, looking a differences between calling only addToBackStack() and calling setRetainInstance(true) as well, and seeing what happens on configuration change:
Calling addToBackStack() but not setRetainInstance(true);
onCreate() and onDestroy() are called.
a bundle passed from onSaveInstanceState() is received as a parameter in onCreateView().
Calling both addToBackStack() and setRetainInstance(true):
onCreate() and onDestroy() are not called. This is metioned in the API docs.
a bundle passed from onSaveInstanceState() is not received in onCreateView(). The passed-in Bundle is null.
A test with logged method calls and parameters tested for null:
In the Activity:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyFragment fragment;
if (savedInstanceState != null) {
fragment = (MyFragment) getFragmentManager().findFragmentByTag("my_fragment_tag");
} else {
fragment = new MyFragment();
FragmentTransaction t = getFragmentManager().beginTransaction();
t.addToBackStack(null);//toggle this
t.add(android.R.id.content, fragment, "my_fragment_tag").commit();
}
}
In the Fragment:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);//toggle this
}
and
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putString("test", "value");
super.onSaveInstanceState(outState);
}
Test 1: Fragment lifecycle when addToBackStack() is called , and setRetainInstance(true) is not called
onAttach()
onCreate()
onCreateView()
onActivityCreated()
onStart()
onResume()
[Device rotated from portrait to landscape]
onPause()
onSaveInstanceState()
onStop()
onDestroyView()
onDestroy()
onDetach()
onAttach()
onCreate()
onCreateView() with bundle param != null
onStart()
onResume()
Test 2 & 3: Fragment lifecycle calls with setRetainInstance(true) called, addToBackStack() called / not called (same result):
onAttach()
onCreateView()
onActivityCreated()
onStart()
onResume()
[Device rotated from portrait to landscape]
onPause()
onSaveInstanceState()
onStop()
onDestroyView()
onDetach()
onAttach()
onCreateView() with bundle param == null
onStart()
onResume()