I'm using navigation in MainActivity, then I start SecondActivity (for result). After finish of SecondActivity I would like to continue with navigation in MainActivity, but FragmentManager has saved his state already.
On Navigation.findNavController(view).navigate(R.id.action_next, bundle) I receive log message:
Ignoring navigate() call: FragmentManager has already saved its state
How I can continue in navigation?
You must always call super.onActivityResult() in your Activity's onActivityResult. That is what:
Unlocks Fragments so they can do fragment transactions (i.e., avoid the state is already saved errors)
Dispatches onActivityResult callbacks to Fragments that called startActivityForResult.
Finally, I fix the issue by simple calling super.onPostResume() right before navigating to restore state.
I've solved this problem this way:
#Override
public void onActivityResult() { //inside my fragment that started activity for result
model.navigateToResults = true; //set flag, that navigation should be performed
}
and then
#Override
public void onResume() { //inside fragment that started activity for result
super.onResume();
if(model.navigateToResults){
model.navigateToResults = false;
navController.navigate(R.id.action_startFragment_to_resultsFragment);
}
}
not sure, if this is not a terrible hack, but it worked for me. FramgentManager state is restored at this point (onResume) and no problems with navigation occur.
I believe above solutions should work. But my problem was different. There was a third party sdk which was launching its activity using context provided by me and it was delivering the result on a listener which I had to implement.
So there was no option for me to work with onActivityResult :(
I used below hack to solve the issue:
private var runnable: Runnable? = null // Runnable object to contain the navigation code
override fun onResume() {
super.onResume()
// run any task waiting for this fragment to be resumed
runnable?.run()
}
override fun responseListener(response: Response) { // Function in which you are getting response
if (!isResumed) {
// add navigation to runnable as fragment is not resumed
runnable = Runnable {
navController.navigate(R.id.destination_to_navigate)
}
} else {
// navigate normally as fragment is already resumed
navController.navigate(R.id.destination_to_navigate)
}
}
Let me know if there is any better solution for this. Currently I found this very simple and easy to implement :)
call super.onPostResume() before navigation....It's working
Related
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()
}
How can I Add a check to prevent this error
I am getting the error in this code:
private fun clearFragmentsFromContainer() {
if(supportFragmentManager.backStackEntryCount>0) {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
}
Error on the line:
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
Log:
java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
at android.support.v4.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:2080)
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:2106)
at android.support.v4.app.FragmentManagerImpl.popBackStack(FragmentManager.java:832)
at com.caring2u.organizer.ui.activities.screen.ActSummaryEvent.clearFragmentsFromContainer(ActSummaryEvent.kt:524)
at com.caring2u.organizer.ui.activities.screen.ActSummaryEvent.onClickEventTabs(ActSummaryEvent.kt:466)
at com.caring2u.organizer.ui.activities.screen.ActSummaryEvent.dataEventsList(ActSummaryEvent.kt:162)
at com.caring2u.organizer.network.retrofit.retrofitTasks.RetroEventsSummary$initiate$1.onResponse(RetroEventsSummary.kt:62)
You are trying to change the fragment stack after onPause as can be seen from the log.
You can either use FragmentManger.commitAllowingStateLoss or be sure to not call this method after onPause
To remove all fragments in a container please use below code
for (Fragment fragment:getSupportFragmentManager().getFragments()) {
if (fragment instanceof NavigationDrawerFragment) {
continue;
}
else if (fragment!=null) {
getSupportFragmentManager().beginTransaction().remove(fragment).commit();
}
}
Probably your fragment transaction is committing after onSavedInstanceState() callback. That means that your activity is stopping and you're adding your fragment in a state that cannot be saved. Actually, during onSavedInstanceState() call Android takes a snapshot of your activity state, this means that if you commit a transaction after the state it's saved the transaction won't be remembered as it was never recorded. From the user point of view that will result in a UI state loss.
Instead of using commitAllowingStateLoss you should understand if you're calling your clearFragmentsFromContainer method from an asynchronous method, in that case probably you should simply move your transaction from the async method.
More about "commit state loss":
AndroidDesignPatterns.com
Elye on Medium
In order to understand if your activity has already called onSaveInstanceState() method, you might think to place a flag inside onSaveInstanceState callback, resetting the flag in the dual method onRestoreInstanceState, something like:
val saveInstanceStateCalled = false
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
saveInstanceStateCalled = true
}
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
saveInstanceStateCalled = false
}
Then you can check the flag before calling clearFragmentsFromContainer
I solved this using the code
if (!fragmentManager.isStateSaved()) {
if(supportFragmentManager.backStackEntryCount>0) {
fragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
}
}
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've an Activity A that contains a Login Fragment and an Activity B that contains a Home Fragment.
I've to start B from Login Fragment after a succesfully login request (async).
I've a callback listener inside the login fragment:
onSuccess(result) {
startActivity(B);
}
Today I met this nice bug: getting exception "IllegalStateException: Can not perform this action after onSaveInstanceState".
I think that's not properly a bug, anyway I don't know how to workaround that. This blog post suggests to avoid transaction inside async callback methods, yeah but how? commitAllowingStateLoss() should be used as a last resort: in case, should I use it inside Home Fragment transaction in Activity B creation method?
Basically, what should I do to start another activity after async callback?
You should use onPostExecute(result) in the AsyncTask:
private class LoginTask extends AsyncTask<parameters,...> {
...
protected void onPostExecute(Long result) {
//if result successful start ActivityB
}
}
Onpost fires after the asynctask is complete.
It runs on the UI thread so that should solve your problem.
Put this in your main activity:
public void run(){
//code you would normally have after task completes
}
Then put this in your onSuccess:
mainactivity.runUIonthread()
I have a tabhost on my application and I'm using an Activity group which handles 3 activities inside.
Example:
ActivityGroup Handles
A -> B -> C
When i start this activities i'm using the flag Intent.FLAG_ACTIVITY_CLEAR_TOP.
My problem is when the user goes from A->B->C and press back button, my B activity shows up, but it does not resume or reload or refresh. It has the same state as before.
For example if the user goes again to C, C is refreshed, but when from C goes back.... B is not.
On B I have implementend methods such as onResume, onStart, onReestart and debugging it the main thread never goes in there...
And i need to refresh B because C can make changes that change the content displayed on B.
I have googleled this for 3 days and I couldn't found a solution..
I had this problem too.
I was using ActivityGroup code based on this blog post.
When I pressed the back button the pervious View would load fine, but the activity associated with it would not fire the onResume().
I was using an extended activity with on overridden and public onResume().
I found this blog post, so tried casting the view as my extended activity and called onResume().
Bingo.
Edit.... here's some more detail...
public class YFIMenuListActivity extends ListActivity {
....
#Override
public void onResume() {
super.onResume();
}
....
}
onResume() is normally protected, but I override it and make it public so that my ActivityGroup can call it.
I only have extended list activities in this activity group (I was just playing around). If you have different activities, each will have to override onResume() and I guess you'd have to look at the type of context you got back from v.getContext() before casting and calling it.
My ActivityGroup looks something like this:
public class BrowseGroup extends ActivityGroup {
.....
#Override
protected void onResume() {
super.onResume();
// call current activity's onResume()
View v = history.get(history.size()-1);
YFIMenuListActivity currentActivity = (YFIMenuListActivity)v.getContext();
currentActivity.onResume();
}
....
}
I've managed to implement an expanded version of cousin_itt's approach.
In both of my activities being used within the activity group I changed onResume from :
protected void onResume()
to
public void onResume()
I then wrote the following onResume function in my ActivityGroup to manually fire off onResumes:
#Override
protected void onResume() {
super.onResume();
View v = history.get(history.size()-1);
MainPeopleView currentActivity = null;
try {
currentActivity = (MainPeopleView)v.getContext();
currentActivity.onResume();
}
catch ( ClassCastException e ) {
Log.e(TAG, e.toString());
}
ProfileView otherActivity = null;
try {
otherActivity = (ProfileView)v.getContext();
otherActivity.onResume();
}
catch ( ClassCastException e ) {
Log.e(TAG, e.toString());
}
}
I have to say, this feels like the worst android hack I've ever written. I'm never using activitygroup again.
((ReportActivity)getLocalActivityManager().getActivity("ReportActivity")).onResume();
ReportActivity is that name you want to back Activity
ps: v.getContext();
only return the ActivityGroup ,it can't invoke child Activity onResume
I have found that onFocusChanged(boolean hasFocus) is great for situations like ActivityGroup. This will fire, even if onResume() does not. I use it for a few of my apps that have TabHosts and ActivityGroups. Here you can force the refresh and insure that it always gets fired when your Activity regains the focus.
I hope you have write your refresh data code in this method onResume().