Android Fragment transaction in background - android

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)

Related

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()
}

Execute function after FragmentManager commit

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);
}
}
});

Ignoring navigate() call: FragmentManager has already saved its state

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

FragmentManager is already executing transactions. When is it safe to initialise pager after commit?

I have an activity hosting two fragments. The activity starts off showing a loader while it loads an object. The loaded object is then passed to both fragments as arguments via newInstance methods and those fragments are attached.
final FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
trans.replace(R.id.container1, Fragment1.newInstance(loadedObject));
trans.replace(R.id.container2, Fragment2.newInstance(loadedObject));
trans.commit();
The second fragment contains a android.support.v4.view.ViewPager and tabs. onResume we initialise it like follows
viewPager.setAdapter(adapter);
viewPager.setOffscreenPageLimit(adapter.getCount()); //the count is always < 4
tabLayout.setupWithViewPager(viewPager);
The problem is android then throws
java.lang.IllegalStateException: FragmentManager is already executing
transactions
With this stack trace: (I took android.support out of the package names just for brevity)
v4.app.FragmentManagerImpl.execSingleAction(FragmentManager.java:1620)
at
v4.app.BackStackRecord.commitNowAllowingStateLoss(BackStackRecord.java:637)
at
v4.app.FragmentPagerAdapter.finishUpdate(FragmentPagerAdapter.java:143)
at v4.view.ViewPager.populate(ViewPager.java:1235)
at v4.view.ViewPager.populate(ViewPager.java:1083)
at
v4.view.ViewPager.setOffscreenPageLimit(ViewPager.java:847)
The data shows if setOffscreenPageLimit(...); is removed. Is there another way to avoid this issue?
When in the lifecycle is the fragment transaction complete so that I can wait to setup my pager?
Simply use childFragmentManger() for viewpager inside a Fragment
mPagerAdapter = new ScreenSlidePagerAdapter(getChildFragmentManager());
mPager.setAdapter(mPagerAdapter);
I had this exception when quickly replacing 2 fragments AND using executePendingTransactions(). Without calling this there was no exception.
What was my case?
I open a fragment A and in its onResume() (under a condition) I ask the activity to replace the fragment with fragment B. At that point the exception occurs.
My solution was to use a Handler.post(runnable) which places the query on the end of the thread queue instead of running it immediately. This way we ensure that the new transaction will be executed after any previous transactions are completed.
So my solution was as simple as:
Handler uiHandler = new Handler(Looper.getMainLooper());
uiHandler.post(new Runnable()
{
#Override
public void run()
{
openFragmentB(position);
}
});
If you're targeting sdk 24 and above you can use:
FragmentTransaction.commitNow()
instead of commit()
If you're targeting older versions, try calling:
FragmentManager.executePendingTransactions()
after the call to commit()
I Had a similar issue,
A mainAcitivity adding fragmentA.
Then fragmentA callback mainactivity to replace itself with fragmentB.
MainActivity throw exception fragmentmanager already executing transaction when replace and commit transaction with fragmentB.
The issue actually comes from fragmentB.
I have a TabHost in fragment B which require getfragmentmanager() to add tabfragment.
Replace getfragmentmanager() by getchildfragmentmanager() in fragmentB solve the issue.
I had same problem. However, I was using the FragmentStateAdapter constructor that gets only the FragmentActivity:
package androidx.viewpager2.adapter;
public FragmentStateAdapter(
#NonNull FragmentActivity fragmentActivity
) {
this(fragmentActivity.getSupportFragmentManager(), fragmentActivity.getLifecycle());
}
Even though it gets the Lifecycle from fragmentActivity instance, it was not working at all.
Then, diving into the code of the FragmentStateAdapter, I saw there is another constructor where you can pass the Lifecycle instance.
package androidx.viewpager2.adapter;
public FragmentStateAdapter(
#NonNull FragmentManager fragmentManager,
#NonNull Lifecycle lifecycle
) {
mFragmentManager = fragmentManager;
mLifecycle = lifecycle;
super.setHasStableIds(true);
}
So, I changed my PageAdapter constructor to receive the FragmentManager as childFragmentManager and the LifeCycle from the viewLifecycleOwner. And pass those parameters to the FragmentStateAdapter constructor:
class MyPagerAdapter(
fragmentManager: FragmentManager,
lifecycle: Lifecycle
) : FragmentStateAdapter(fragmentManager, lifecycle) {
//...
}
Then, when calling my PagerAdapter constructor, I pass the FragmentManager and
I get the lifeCycle from the viewLifecycleOwner:
val myPagerAdapter = MyPagerAdapter(
fragmentManager = childFragmentManager,
lifecycle = viewLifecycleOwner.lifecycle
)
[UPDATE]
MyPagerAdapter is set from a Fragment.
ViewPager2
It is quite an old answer from the time of ViewPager and has helped many to solve the issue who came across this. Just in case you are having ViewPager2 and face similar issue then,
We know this happens when we have ViewPager2 inside a fragment and FragmentStateAdapter class has a constructor with Fragment parameter, so you can use that to create your adapter.
Like,
class ScreenSlidePagerAdapter(fragment: Fragment): FragmentStateAdapter(fragment)
Then,
val pagerAdapter = ScreenSlidePagerAdapter(this)
Got a similar error not connected to question but hops its helps someone when working with firebase.
Remove activity, requireActivity or this from .addSnapshotListener(), keep it blank.
videosListener = videosCollectionRef
.addSnapshotListener() { snapshot, exception ->
if (exception != null) {
Log.e("Exception", "Could not retrieve documents: $exception")
}
if (snapshot != null) {
parseData(snapshot)
}
}
If anyone is using Robolectric >3.6 and at least through 4.0.2 with a ViewPager you may see this even with correct code.
There is related information in this github issue tracking the problem for Robolectric.
The issue is not resolved as I write this answer, and the only workarounds known are to use #Config(sdk={27}) which appears to work for some but did not work for me, or implement a ViewPagerShadow with a workaround in your test package with the rather-long code referenced on github (I can include it here if that is better but this may not be relevant enough as an answer?) and use #Config(shadows={ShadowViewPager.class})
I used/extended my adapter with FragmentStatePagerAdapter instead of FragmentPagerAdapter this resolved my issue.
Use FragmentStatePagerAdapterif the fragments are dynamic and are changing frequently during run-time.
Use FragmentPagerAdapter if the fragments are static and do NOT change frequently during run-time.
enlightened from this article,
https://medium.com/inloopx/adventures-with-fragmentstatepageradapter-4f56a643f8e0
I had the same issue. Navigation from one fragment to another by add method. The problem was that I added fragment transition in the onViewCreadted method. Then I tried to move the transition in onResume method. That also does not help. So I added transition half-second after the resume. After that everything was fine.
It was a small crush that affected less than half percent of my users. Even though, it was very annoying.
Just do not call
FragmentTransaction#commit() from fragment which is already been in the same FragmentManager at another transaction process
For ex:
Activity:
override fun onCreate(savedInstanceState: Bundle?) {
val fragment = MyFragment()
setFragment(fragment)
}
fun setFragment(fragment: Fragment){
supportFragmentManager.beginTransaction()
.replace(...,fragment, ...)
.commit()
}
MyFragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/*
* here error will occurs, because the fragment MyFragment is in current transaction
*/
activity?.setFragment(AnotherFragment())//error
}
Solution:
Do this:
Activity:
override fun onCreate(savedInstanceState: Bundle?) {
setFragment(MyFragment())
...
setFragment(AnotherFragment())
}
fun setFragment(fragment: Fragment){
supportFragmentManager.beginTransaction()
.replace(...,fragment, ...)
.commit()
}

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