Execute function after FragmentManager commit - android

I have a toolbar with text that is updated after an item in my menu is selected.
The problem is that I call the function that handles the title in my toolbar right after my transaction like this:
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, fragment)
.addToBackStack(null)
.commit()
setTitle()
However, when my function is called, the Fragment I get is always the one currently displayed and not the new one.
supportFragmentManager.fragments.last()
I'm guessing the Fragments transaction takes place in a Thread. How do I wait until it's complete to perform my function?

If you're using KTX extensions you can wait for fragment to be created using LifecycleScope instead of listening to fragment manager directly.
First ensure you have fragment-ktx dependency in your app level build.gradle:
dependencies {
...
implementation 'androidx.fragment:fragment-ktx:1.2.1'
...
}
Then you can perform a suspended execution that waits for your fragment to be created:
supportFragmentManager.beginTransaction()
.replace(R.id.fragment, fragment)
.addToBackStack(null)
.commit()
fragment.lifecycleScope.launchWhenCreated {
setTitle()
}

Since you are not adding your fragment to the back stack you cannot use the built-in FragmentManager.OnBackStackChangedListener so instead you can either call
executePendingTransactions() right after you commit the transaction or use commitNow() method instead of commit() to do the same, then handle your toolbar changes with setTitle().

i think i respond to your question so late but it could help someone who want the same solution :
fragment.getLifecycle().addObserver(new LifecycleEventObserver() {
#Override
public void onStateChanged(#NonNull LifecycleOwner source, #NonNull Lifecycle.Event event) {
if(event.getTargetState() == Lifecycle.State.RESUMED){
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
setTitle();
}
}, 500);
}
}
});

Related

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)

Can this collection add Fragment codeblock become any simpler (Kotlin)

I am new to Kotlin. I wrote this code block to add a fragment when there are no fragments in the backstack. My code block however looks a lil clumsy. Can it be written in a better way?
with(supportFragmentManager) {
takeIf { backStackEntryCount == 0 }
?. apply {
with(beginTransaction()) {
add(R.id.container, FirstFragment())
commit()
}
}
}
also, with(beginTransaction()) shows me lint warning that transaction should be committed(). I think it has to do with my incorrect implementation of it. Kindly tell me how to get rid of that lint warning.
EDIT 1
Not here but at certain places I do perform some other operations along with add(). Stuff like adding the fragment to backstack and providing custom Transitions.
activity?.supportFragmentManager?.let {
with(it.beginTransaction()) {
setCustomAnimations(R.anim.slide_in_left, R.anim.slide_out_left,
R.anim.slide_in_right, R.anim.slide_out_right)
addToBackStack("second")
replace(R.id.container, SecondFragment())
commit()
}
}
Now there are a few methods that need to be called on FragmentTransaction object. This one is more suitable for my case.
How about
if(savedInstanceState == null) { // replaces the `takeIf` things
supportFragmentManager.beginTransaction()
.add(R.id.container, FirstFragment())
.commit()
}
you know kotlin provide extension function feature that used to make easy way to add fragement like below code ..
fun Fragment.addReportFragment(): Fragment {
mFragment = ReportFragment()
this.fragmentManager?.beginTransaction()?.replace(if (this is ReportFragment) LOGIN_ROOT else MAIN_ROOT, mFragment, REPORT_FRAGMENT)?.commit()
return mFragment as ReportFragment
}
and simple way to used below code..
var pickpowerfrag = PickPowerFragment()
fragmentManager.beginTransaction()
.replace(R.id.ReplaceFrame, pickpowerfrag)
.addToBackStack(null)
.commit()

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)

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

How to get a Fragment to remove itself, i.e. its equivalent of finish()?

I'm converting an app to use fragments using the compatibility library.
Now currently I have a number of activities (A B C D) which chain onto one another, D has a button 'OK' which when pressed calls finish which then bubbles up through onActivityResult() to additionally destroy C and B.
For my pre Honycomb fragment version each activity is effectively a wrapper on fragments Af Bf Cf Df. All activities are launched via startActivityForResult() and onActivityResult() within each of the fragments can happily call getActivity().finish()
The problem that I am having though is in my Honeycomb version I only have one activity, A, and fragments Bf, Cf, Df are loaded using the FragmentManager.
What I don't understand is what to do in Df when 'OK' is pressed in order to remove fragments Df, Cf, and Bf?
I tried having the fragment popping itself off the stack but this resulted in an exception. onActivityResult() is useless because I have not loaded up the fragment using startActivityForResult().
Am I thinking about this completely the wrong way? Should I be implementing some sort of listener that communicates with either the parent fragment or activity in order to do the pop using the transaction manager?
While it might not be the best approach the closest equivalent I can think of that works is this with the support/compatibility library
getActivity().getSupportFragmentManager().beginTransaction().remove(this).commit();
or
getActivity().getFragmentManager().beginTransaction().remove(this).commit();
otherwise.
In addition you can use the backstack and pop it. However keep in mind that the fragment might not be on the backstack (depending on the fragmenttransaction that got it there..) or it might not be the last one that got onto the stack so popping the stack could remove the wrong one...
You can use the approach below, it works fine:
getActivity().getSupportFragmentManager().popBackStack();
What I don't understand is what to do in Df when 'OK' is pressed in order to remove fragments Df, Cf, and Bf?
Step #1: Have Df tell D "yo! we got the OK click!" via calling a method, either on the activity itself, or on an interface instance supplied by the activity.
Step #2: Have D remove the fragments via FragmentManager.
The hosting activity (D) is the one that knows what other fragments are in the activity (vs. being in other activities). Hence, in-fragment events that might affect the fragment mix should be propagated to the activity, which will make the appropriate orchestration moves.
You should let the Activity deal with adding and removing Fragments, as CommonsWare says, use a listener. Here is an example:
public class MyActivity extends FragmentActivity implements SuicidalFragmentListener {
// onCreate etc
#Override
public void onFragmentSuicide(String tag) {
// Check tag if you do this with more than one fragmen, then:
getSupportFragmentManager().popBackStack();
}
}
public interface SuicidalFragmentListener {
void onFragmentSuicide(String tag);
}
public class MyFragment extends Fragment {
// onCreateView etc
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
suicideListener = (SuicidalFragmentListener) activity;
} catch (ClassCastException e) {
throw new RuntimeException(getActivity().getClass().getSimpleName() + " must implement the suicide listener to use this fragment", e);
}
}
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Attach the close listener to whatever action on the fragment you want
addSuicideTouchListener();
}
private void addSuicideTouchListener() {
getView().setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
suicideListener.onFragmentSuicide(getTag());
}
});
}
}
In the Activity/AppCompatActivity:
#Override
public void onBackPressed() {
if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) {
// if you want to handle DrawerLayout
mDrawerLayout.closeDrawer(GravityCompat.START);
} else {
if (getFragmentManager().getBackStackEntryCount() == 0) {
super.onBackPressed();
} else {
getFragmentManager().popBackStack();
}
}
}
and then call in the fragment:
getActivity().onBackPressed();
or like stated in other answers, call this in the fragment:
getActivity().getSupportFragmentManager().beginTransaction().remove(this).commit();
If you are using the new Navigation Component, is simple as
findNavController().popBackStack()
It will do all the FragmentTransaction in behind for you.
See if your needs are met by a DialogFragment. DialogFragment has a dismiss() method. Much cleaner in my opinion.
I create simple method for that
popBackStack(getSupportFragmentManager());
Than place it in my ActivityUtils class
public static void popBackStack(FragmentManager manager){
FragmentManager.BackStackEntry first = manager.getBackStackEntryAt(0);
manager.popBackStack(first.getId(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}
It's work great, have fun!
OnCreate:
//Add comment fragment
container = FindViewById<FrameLayout>(Resource.Id.frmAttachPicture);
mPictureFragment = new fmtAttachPicture();
var trans = SupportFragmentManager.BeginTransaction();
trans.Add(container.Id, mPictureFragment, "fmtPicture");
trans.Show(mPictureFragment); trans.Commit();
This is how I hide the fragment in click event 1
//Close fragment
var trans = SupportFragmentManager.BeginTransaction();
trans.Hide(mPictureFragment);
trans.AddToBackStack(null);
trans.Commit();
Then Shows it back int event 2
var trans = SupportFragmentManager.BeginTransaction();
trans.Show(mPictureFragment); trans.Commit();
If you need to popback from the fourth fragment in the backstack history to the first, use tags!!!
When you add the first fragment you should use something like this:
getFragmentManager.beginTransaction.addToBackStack("A").add(R.id.container, FragmentA).commit()
or
getFragmentManager.beginTransaction.addToBackStack("A").replace(R.id.container, FragmentA).commit()
And when you want to show Fragments B,C and D you use this:
getFragmentManager.beginTransaction.addToBackStack("B").replace(R.id.container, FragmentB, "B").commit()
and other letters....
To return to Fragment A, just call popBackStack(0, "A"), yes, use the flag that you specified when you add it, and note that it must be the same flag in the command addToBackStack(), not the one used in command replace or add.
You're welcome ;)
To Close a fragment while inside the same fragment
getActivity().onBackPressed();
kotlin -
requireActivity().onBackPressed()
parentFragmentManager.apply {
val f = this#MyFragment
beginTransaction().hide(f).remove(f).commit()
}
Why not just:
getActivity().finish();

Categories

Resources