commitAllowingStateLoss on DialogFragment - android

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

Related

Showing dialog fragment throws "can not perform this action after onSaveInstanceState" exception

Sometimes and on some devices dialog fragment crashes the activity because of the above mentioned illegalstateexception
I have tried shownow() which helped only on some devices, but the problem still exists.
val dialog = CustomDialogFragment.newInstance(false, correctAnswer, true)
dialog.show(supportFragmentManager, "alert")
I need the dialog instance for future use, otherwise I would use show instantly after newInstance() . What is the possible solution?
As I discovered the problem was caused by Android issue, I had the following workaround solution: just override show() method of the dialog fragment as it is below:
#Override
public void show(#NonNull FragmentManager manager, #Nullable String tag) {
FragmentTransaction ft = manager.beginTransaction();
ft.add(this, tag);
ft.commitAllowingStateLoss();
}
The root cause of this issue is you try to show FragmentDialog activity has changed its state to onPause().
To handle this you must check life cycle state before showing your dialog
if(lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)){
val dialog = CustomDialogFragment.newInstance(false, correctAnswer, true)
dialog.show(supportFragmentManager, "alert")
}

FragmentManager.findFragmentByTag() returns null

I have found a specific case when FragmentManager.findFragmentByTag("tag") returns null.
I have a gut feeling it has to do with timing?
I have a networking library with the following callbacks:
onStart()
{
Utils.ShowLoadingDialog("loading");
}
onFinnish()
{
Utils.DismissLoadingDialog("loading");
}
Then in my Utils class I have the following code:
public void showLoadingDialog(String title, String message, String tag) {
DialogFragment loadingDialogFragment = new LoadingDialogFragment();
Bundle args = new Bundle();
args.putString(CommonBundleAttributes.CONNECTING_ACTIVITY_DIALOG_TITLE, title);
args.putString(CommonBundleAttributes.CONNECTING_ACTIVITY_DIALOG_MESSAGE, message);
loadingDialogFragment.setArguments(args);
FragmentTransaction transaction = fragManager.beginTransaction();
loadingDialogFragment.show(transaction, tag);
}
public void dismissLoadingDialog(String tag) {
DialogFragment dg = (DialogFragment) fragManager.findFragmentByTag(tag);
if (dg != null) {
// this reference isn't null so the dialog is available
dg.dismissAllowingStateLoss();
}
}
Now this generally works fine. However in cases when the network layer detects there is no internet. It will throw an error and then immediately call onFinnish(). In this case the Utils.DismissDialog(tag) does nto find the fragment and therefore does not dismiss it?
You can use executePendingTransactions() to wait for the fragment transaction to come through.
public void dismissLoadingDialog(String tag) {
fragManager.executePendingTransactions();
DialogFragment dg = (DialogFragment) fragManager.findFragmentByTag(tag);
if (dg != null) {
// this reference isn't null so the dialog is available
dg.dismissAllowingStateLoss();
}
}
Use TRY-CATCH or even IF statement to check current internet-connection.
This can be case with the committing the transaction.
Just check if in your show method if you have commit the transaction for Dialog fragment or not.
transaction.commit();
Until that your fragment manager does not have fragment to be added in that.
Also you need to make sure you are finding fragment from same Activity's fragment manager to which you committed fragment.

Android prevent DialogFragment opening twice

I am trying to prevent my DialogFragment opening twice. Here is what I do:
I try to keep only one instance of my fragment. I create and add my fragment like this:
//MyFragment.java
public static MyFragment mInstance;
public static void instantiateFragment() {
MyFragment myFragment = MyFragment.getInstance();
if(!myFragment.isAdded()) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(myFragment, TAG);
ft.commit();
}
}
private static MyFragment getInstance() {
if(mInstance == null) {
mInstance = new MyFragment();
}
return mInstance;
}
And when a button is clicked, I intentionally try to add fragment twice like this:
MyFragment.instantiateFragment();
MyFragment.instantiateFragment();
But I get IllegalStateException: Fragment already added. Any ideas about that?
Thanks.
Indeed it's a problem with asynchronous commit of transactions, so as #Android jack stated you can use executePendingTransactions() like in this answer,
or even better use commitNow(),
or try something like this:
public static void instantiateFragment() {
Fragment myFragment = getSupportFragmentManager().findFragmentByTag(TAG);
if (myFragment == null) {
myFragment = MyFragment.getInstance();
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(myFragment, TAG);
ft.commit();
}
}
I think this has to do with the asynchronous behaviour of fragment transactions.Fragment Transactions are committed asynchronously. So at first call, your fragment is added but it is committed asynchronously.Again in your next call your fragment is not added as it is not committed yet so !myFragment.isAdded() returns false.Then while adding the fragment the previous transaction is committed due to which it raises exception.
Try to use this
getFragmentManager().executePendingTransactions();
before your (!myFragment.isAdded()) code.

Fragment showing and Activity lifecycle

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

Can not perform this action after onSaveInstanceState WHEN commit

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{}

Categories

Resources