Why commitNow does not set fragment to null? - android

I have tried to use commitNow for ensuring that fragment transactions are synchronous. However, after my testing, I found that for some reason commitNow does not remove fragments immediately.
I have added a Handler with postDelayed 100 milliseconds and the fragment was still not null. After that, I changed postDelayed to 1000 milliseconds and finally, the fragment was null.
How can I ensure that the fragment is null immediately after I call commitNow? I tried to add supportFragmentManager.popBackStack(), but no help.
If it is impossible to make fragment null immediately, how can I check if that fragment is no longer in the stack, before adding the same fragment?
UPDATE
I found that if I removed setCustomAnimations, then transactions are indeed occurring immediately. How can I observe when setCustomAnimations have finished?
val fragment = supportFragmentManager.findFragmentByTag(DEMO_FRAGMENT)
if (fragment != null) {
Log.d("test", "DEMO_FRAGMENT is not null")
val fragmentTransaction = supportFragmentManager.beginTransaction()
fragmentTransaction.setCustomAnimations(R.anim.custom_fade_in, R.anim.custom_fade_out)
.remove(fragment).commitNow()
supportFragmentManager.popBackStack();
Log.d("test", "is fragment null:${supportFragmentManager.findFragmentByTag(DEMO_FRAGMENT) == null}")
Handler().postDelayed({
Log.d("test", "is fragment null:${supportFragmentManager.findFragmentByTag(DEMO_FRAGMENT) == null}")
}, 1000)
}
}

For that you have to use popBackStackImmediate(). More info here

Related

Android. IllegalStateException: FragmentManager is already executing transactions when show DialogFragment

Here is my code to show DialogFragment from another fragment:
override fun showDialog(dialog: DialogFragment, tag: String?) {
val fragmentManager = fragment.childFragmentManager
if (tag != null) {
fragmentManager.executePendingTransactions()
val fragment = fragmentManager.findFragmentByTag(tag)
if (fragment != null) {
return
}
}
dialog.show(fragmentManager, tag)
}
Everything works well, the dialog is displayed as it should.
However, I observe the following problem:
Caused by: java.lang.IllegalStateException: FragmentManager is already executing transactions
at androidx.fragment.app.FragmentManager.ensureExecReady(FragmentManager.java:1636)
at androidx.fragment.app.FragmentManager.execPendingActions(FragmentManager.java:1695)
at androidx.fragment.app.FragmentManager.executePendingTransactions(FragmentManager.java:564)
at *******************.showDialog()
Suppose after a network request a dialog was shown through which the user went to the system settings, after returning to the screen of my application, the network request is sent again and the dialog is shown again. And as far as I understand, the fragment transaction is ongoing, and at the same time it tries to execute new transaction in order to show DialogFragment.
As a solution to the problem, I found the use childFragmentManager. But that doesn't help me. Please help me find a possible solution to the problem.

Caused by java.lang.IllegalStateException Can not perform this action after onSaveInstanceState

I have an activity and a fragment within that activity. The fragment is loaded within the activity onCreate().
if (!supportFragmentManager.isDestroyed) {
val fragmentTransaction = this.supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.containerLayout, fragment).commit()
}
Inside the fragment, I am performing an API call and when the result is received, the activity gets the callback and the result is passed to the fragment from the activity.
The issue is when I load this activity and when the API is still on call if I press the device recents button then the app crashes showing the below exception.
Caused by java.lang.IllegalStateException Can not perform this action after onSaveInstanceState
I understand that the problem is the fragment tries to commit after onSaveInstanceState is called. But how is that happening I am not clear. I went through the article too. It says three points as solution.
To commit the fragment within onCreate() which I am already doing.
Not to commit in onPostExecute() which is not applicable to me.
Use commitAllowingStateLoss() only as a last resort.
Should I need to change commit() to commitAllowingStateLoss()? As I went through the docs, I don't feel that safe too. Could someone suggest to me the right way?
I didn't use commitAllowingStateLoss(). I put the code as:
var isAnException: Boolean = false
try {
if (!supportFragmentManager.isDestroyed) {
val fragmentTransaction = this.supportFragmentManager.beginTransaction()
fragmentTransaction.replace(R.id.containerLayout, fragment).commit()
}
} catch (exception: IllegalStateException) {
isAnException = true
}
and in onResume() of my activity, I added the below code to make the fragment work when taken from the recents.
override fun onResume() {
if (isAnException) {
isAnException = false
//fragment load and set the views
}
super.onResume()
}

Backstack management : Restarter must be created only during owner's initialization stage

I am using a bottom navigation bar in my MainActivity to handle some fragments. This is the code used for switching between them:
private val mOnNavigationItemSelectedListener = BottomNavigationView.OnNavigationItemSelectedListener { item ->
if (item.isChecked &&
supportFragmentManager.findFragmentById(R.id.act_main_fragment_container) != null
)
return#OnNavigationItemSelectedListener false
val fragment =
when (item.itemId) {
R.id.navigation_home -> fragments[0]
R.id.navigation_bookings -> fragments[1]
R.id.navigation_messages -> fragments[2]
R.id.navigation_dashboard -> fragments[3]
R.id.navigation_profile -> fragments[4]
else -> fragments[0]
}
this replaceWithNoBackStack fragment
return#OnNavigationItemSelectedListener true
}
the method replaceWithNoBackstack is just a short-hand for this:
supportFragmentManager
?.beginTransaction()
?.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
?.replace(containerId, fragment)
?.commit()
The problem is that when i switch faster between them, my app crashes with the following exception:
java.lang.IllegalStateException: Restarter must be created only during owner's initialization stage
at androidx.savedstate.SavedStateRegistryController.performRestore(SavedStateRegistryController.java:59)
at androidx.fragment.app.Fragment.performCreate(Fragment.java:2580)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:837)
at androidx.fragment.app.FragmentManagerImpl.moveFragmentToExpectedState(FragmentManagerImpl.java:1237)
at androidx.fragment.app.FragmentManagerImpl.moveToState(FragmentManagerImpl.java:1302)
at androidx.fragment.app.BackStackRecord.executeOps(BackStackRecord.java:439)
at androidx.fragment.app.FragmentManagerImpl.executeOps(FragmentManagerImpl.java:2075)
at androidx.fragment.app.FragmentManagerImpl.executeOpsTogether(FragmentManagerImpl.java:1865)
at androidx.fragment.app.FragmentManagerImpl.removeRedundantOperationsAndExecute(FragmentManagerImpl.java:1820)
at androidx.fragment.app.FragmentManagerImpl.execPendingActions(FragmentManagerImpl.java:1726)
at androidx.fragment.app.FragmentManagerImpl$2.run(FragmentManagerImpl.java:150)
at android.os.Handler.handleCallback(Handler.java:789)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6709)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:769)
I've been searching a lot and couldn't find an answer.
I also got this error if I do an API call, put the app in background, wait for the response, and at the time I go back to the app, the app crashes because I am trying to display a dialog fragment immediately (the reason I think this is happening is that the transaction of recreating the fragment when coming back from the background is still in progress at the time of displaying the dialog fragment). I solved this in a hacky way by setting a 500ms delay for the dialog because I couldn't figure out other solutions.
Please ask if you need more details regarding this.
Thank you in advance!
POSSIBLE TEMP SOLUTIONS
EDIT
I solved this issue by downgrading the app compat depedency to androidx.appcompat:appcompat:1.0.2 but this is just a temporary solution, since i will have to update it in future. I'm hoping someone will figure it out.
EDIT 2
I solved the issue by removing setTransition() from fragment transactions. At least I know the reason why android apps does not have good transitions in general
EDIT 3
Maybe the best solution to avoid this issue and also make things work smoothly is just to use ViewPager to handle bottom bar navigation
because the version 1.0.0 has not check the state, so it will not throw the exception,
but the version 1.1.0 changes the source code,so it throws the exception.
this is the Fragment version-1.1.0 source code, it will invoke the method performRestore
void performCreate(Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mState = CREATED;
mCalled = false;
mSavedStateRegistryController.performRestore(savedInstanceState);
onCreate(savedInstanceState);
mIsCreated = true;
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onCreate()");
}
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
/**
the exception
**/
public void performRestore(#Nullable Bundle savedState) {
Lifecycle lifecycle = mOwner.getLifecycle();
if (lifecycle.getCurrentState() != Lifecycle.State.INITIALIZED) {
throw new IllegalStateException("Restarter must be created only during "
+ "owner's initialization stage");
}
lifecycle.addObserver(new Recreator(mOwner));
mRegistry.performRestore(lifecycle, savedState);
}
this is the version-1.0.0 source code,did not invoke the performRestore,so will not throw the exception
void performCreate(Bundle savedInstanceState) {
if (mChildFragmentManager != null) {
mChildFragmentManager.noteStateNotSaved();
}
mState = CREATED;
mCalled = false;
onCreate(savedInstanceState);
mIsCreated = true;
if (!mCalled) {
throw new SuperNotCalledException("Fragment " + this
+ " did not call through to super.onCreate()");
}
mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
There are two different solution which can handle this:
The first solution is to split the transaction。
Because we always use replace or merge remove and add into one Transaction.
We can split the transaction to two transaction like this:
FragmentTransaction ft = manager.beginTransaction();
Fragment prev = manager.findFragmentByTag(tag);
if (prev != null) {
//commit immediately
ft.remove(prev).commitAllowingStateLoss();
}
FragmentTransaction addTransaction = manager.beginTransaction();
addTransaction.addToBackStack(null);
addTransaction.add(layoutId, fragment,
tag).commitAllowingStateLoss();
because this two transaction will be two different Message which will be handled by Handler.
The second solution is check the state in advance.
we can follow the source code,check the state in advance
FragmentTransaction ft = manager.beginTransaction();
Fragment prev = manager.findFragmentByTag(tag);
if (prev != null) {
if (prev.getLifecycle().getCurrentState() != Lifecycle.State.INITIALIZED) {
return;
}
ft.remove(prev);
}
I recommend the first way,because the second way is folowing the source code,if the source
code change the code, it will be invalid。
I had the same problem.
val fragment = Account.activityAfterLogin
val ft = activity?.getSupportFragmentManager()?.beginTransaction()
//error
ft?.setCustomAnimations(android.R.anim.slide_in_left,android.R.anim.slide_out_right)0
ft?.replace(R.id.framelayout_account,fragment)
ft?.commit()
Changing the library version did not help.
I solved this by adding the ft?.AddToBackStack(null) line after the ft?.setCustomAnimations () method and that’s it.
Animation works and there are no crashes.
If you're using 'androidx.core:core-ktx:1.0.2',
try changing to 1.0.1
If you're using lifecycle(or rxFragment) and androidx_appcompat:alpha05, try changeing versio.
ex) appcompat : 1.1.0-beta01 or 1.0.2
I think's that it appears as an error when saving the state when the target fragment is reused (onPause-onResume).
I changed implementation to api for androidx.appcompat:appcompat:1.0.2 and its worked for me
If it can help, I have encountered the same issue with a BottomNavigationView and setCustomAnimations, basically by switching quickly between Fragments, you may end up starting a FragmentTransaction while the previous one has not finished and then it crashes.
To avoid that, I disable the Navigation Bar until the transition is finished. So I have created a method to enable/disable the BottomNavigationView items (disabling the BottomNavigationView itself does not disable the menu or I didn't find the way) and then I re-enable them once the transition is completed.
To disable the items I call the following method right before starting a FragmentTransition:
public void toggleNavigationBarItems(boolean enabled) {
Menu navMenu = navigationView.getMenu();
for (int i = 0; i < navMenu.size(); ++i) {
navMenu.getItem(i).setEnabled(enabled);
}
}
To re-enable them, I have created an abstract Fragment class for the Fragments loaded from the BottomNavigationView. In this class, I overrides onCreateAnimator (if you use View Animation you should override onCreateAnimation) and I re-enable them onAnimationEnd.
#Nullable
#Override
public Animator onCreateAnimator(int transit, boolean enter, int nextAnim) {
if(enter){ // check the note below
Animator animator = AnimatorInflater.loadAnimator(getContext(), nextAnim);
animator.addListener(new AnimatorListenerAdapter() {
#Override
public void onAnimationEnd(Animator animation) {
myActivity.toggleNavigationBarItems(true)
}
});
return animator;
}
return super.onCreateAnimator(transit, enter, nextAnim);
}
Note: as my enter and exit animations have the same duration, I don't need to synchronise them as the enter animation starts after the exit one. That's why the if (enter) is sufficient.
I fixed this problem with add 'synchronized' into add fragment method
before :
public void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
try {
if (!fragment.isAdded()) {
if (param != null) {
fragment.setArguments(param);
}
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
.add(contentFrameId, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (addToStack)
fragmentTransaction.addToBackStack(fragment.getClass().toString());
fragmentTransaction.commit();
}
} catch (IllegalStateException e) {
handleError(e.getMessage());
} catch (Exception e) {
handleError(e.getMessage());
}
}
after :
public synchronized void addFragment(int contentFrameId, Fragment fragment, Bundle param, boolean addToStack) {
try {
if (!fragment.isAdded()) {
if (param != null) {
fragment.setArguments(param);
}
FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction()
.add(contentFrameId, fragment)
.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
if (addToStack)
fragmentTransaction.addToBackStack(fragment.getClass().toString());
fragmentTransaction.commit();
}
} catch (IllegalStateException e) {
handleError(e.getMessage());
} catch (Exception e) {
handleError(e.getMessage());
}
}
This bug seems to be resolved using androidx.appcompat:appcomat:1.1.0-rc01 and androidx.fragment:fragment:1.1.0-rc03
https://developer.android.com/jetpack/androidx/releases/fragment#1.1.0-rc03
I have this issue when using setCustomAnimations.
by removing setCustomAnimations solved my problem.
also I have no problem when I create new instance of fragment before showing it even using setCustomAnimation.
EDIT: another way is adding fragment to backstack.
I was able to fix this (hopefully 😃) by using commitNow() instead of commit() for all bottom nav fragment transactions.
I like this approach better as it allows you to still use custom transitions between fragments.
Note: This is a solution only if you don't want your bottom nav transactions to be added to backstack (which you should not be doing anyways).
Nothing worked except Drown Coder's solution, but it was still not perfect, because it adds transactions to backstack. So if you press all buttons in bottom navigation, you have at least 1 of every fragment in backstack. I slightly improved this solution, so you don't use .replace() that crashes app whith thansaction animations.
Here is the code:
if (getChildFragmentManager().getBackStackEntryCount() > 0) {
getChildFragmentManager().popBackStack();
}
FragmentTransaction addTransaction = getChildFragmentManager().beginTransaction();
addTransaction.setCustomAnimations(R.animator.fragment_fade_in, R.animator.fragment_fade_out);
addTransaction.addToBackStack(null);
addTransaction.add(R.id.frame, fragment, fragment.getClass().getName()).commitAllowingStateLoss();
I found another way of creating this case.
CASE-1
Inflate a fragment in frame-layout at an activity
start an API request (don't consume the api response when app in foreground)
Keep your app in background
Consume the API request (suppose you want to add another fragment on api response)
Inflate another fragment using .replace() method on the same frame-layout
You will be able to create the Crash
CASE-2
Inflate a fragment in frame-layout at an activity
Start an API request
Consume the api in foreground (suppose you want to add another fragment on api response, using .replace() method of fragment-manager)
Put your app in background
Recreate your application (you can do this using "Don't keep activities", changing permission, changing system language)
Come back to your application
Your activity will start re-creating
Activity will auto recreate its already inflated fragment suppose it is of (point-1)
Make sure API is request again in on recreate case, after point-8
Consume API response and inflate another fragment using .replace() method
You will be able to create the Crash (As in this case, already a transition is running point-8, and you are adding another fragment at point-10)

Android Fragment transaction in background

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)

IllegalStateException when adding & replacing Fragments; commit vs commitAllowingStateLoss

It is rare that this happens, but occasionally my app will crash due to an IllegalStateException when adding and replacing fragments. Here is how I am doing it, I do so with an animation.
private void addFragmentReplace(int containerId, Fragment fragment) {
// check if the fragment has been added already
Fragment temp = mFragmentManager.findFragmentByTag(fragment.getTag());
if (!Utils.checkIfNull(temp) && temp.isAdded()) {
return;
}
// replace fragment and transition with animation
mFragmentManager.beginTransaction().setCustomAnimations(R.anim.ui_slide_in_from_bottom_frag,
R.anim.ui_slide_out_to_bottom_frag).replace(containerId, fragment).addToBackStack(null)
.commit();
}
I have researched into changing "commit()" to "commitAllowingStateLoss()" but is that really a solution? It will prevent the crashes, however, won't it cause other conflicts such as the fragment not displaying at times or other? Is the following an odd improvement to my above code snippet?
private void addFragmentReplace(int containerId, Fragment fragment) {
// check if the fragment has been added already
Fragment temp = mFragmentManager.findFragmentByTag(fragment.getTag());
if (!Utils.checkIfNull(temp) && temp.isAdded()) {
return;
}
// replace fragment and transition with animation
try {
mFragmentManager.beginTransaction().setCustomAnimations(R.anim.ui_slide_in_from_bottom_frag,
R.anim.ui_slide_out_to_bottom_frag).replace(containerId, fragment).addToBackStack(null)
.commit();
} catch (IllegalStateException e) {
e.printStackTrace();
mFragmentManager.beginTransaction().setCustomAnimations(R.anim.ui_slide_in_from_bottom_frag,
R.anim.ui_slide_out_to_bottom_frag).replace(containerId, fragment).addToBackStack(null)
.commitAllowingStateLoss();
}
}
Thanks in advance. My concerns come from the documentation for commitAllowingStateLoss() which reads
Like {#link #commit} but allows the commit to be executed after an
activity's state is saved. This is dangerous because the commit can
be lost if the activity needs to later be restored from its state, so
this should only be used for cases where it is okay for the UI state
to change unexpectedly on the user.
Some tips or advice on this would be appreciated. Thanks!

Categories

Resources