I have a parent Fragment Activity that has a ViewPager which contains a child ViewPager. The child ViewPager contains Fragments for each page. I communicate between these child page fragments and the top parent Fragment Activity using a callback interface e.g.
public interface Callbacks {
public void onItemSelected(Link link);
}
In the parent Fragment Activity I listen for onItemSelected events e.g.
#Override
public void onItemSelected(Link link) {
Bundle argumentsFront = new Bundle();
argumentsFront.putParcelable(FragmentComments.ARG_ITEM_ID, link);
fragmentComments = new FragmentComments();
fragmentComments.setArguments(argumentsFront);
getSupportFragmentManager().beginTransaction().replace(R.id.post_container, fragmentComments).commitAllowingStateLoss();
}
Now this works fine when the app is first launched.
If you turn the device to change the orientation the Activity restarts. All fragments reinitialise themselves as I use setRetainInstance(true); (I do not call setRetainInstance(true) in the page Fragments of the child ViewPager as it is not supported). However if I click a list item in the Fragment of the child ViewPager I get this exception:
FATAL EXCEPTION: main
java.lang.IllegalStateException: Activity has been destroyed
at android.support.v4.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1342)
at android.support.v4.app.BackStackRecord.commitInternal(BackStackRecord.java:595)
at android.support.v4.app.BackStackRecord.commitAllowingStateLoss(BackStackRecord.java:578)
Does anyone know why this happens?
Thanks
When you rotate the device, Android saves, destroys, and recreates your Activity and its ViewPager of Fragments. Since the ViewPager uses the FragmentManager of your Activity, it saves and reuses those Fragments for you (and does not create new ones), so they will hold the old references to your (now destroyed) original Activity, and you get that IllegalStateException.
In your child Fragments, try something like this:
#Override
public void onAttach(Activity activity) {
super.onAttach(activity);
Log.v(TAG, "onAttach");
// Check if parent activity implements our callback interface
if (activity != null) {
try {
mParentCallback = (Callbacks) activity;
}
catch (ClassCastException e) {
}
}
}
Then when a selection occurs:
if(mParentCallback != null) {
mParentCallback.onItemSelected(selectedLink);
}
Since onAttach gets called as part of the Fragment lifecycle, your Fragments will update their callback reference on rotation.
I had a similar issue, I think it is because the fragments are retained and are keeping a reference to a destoryed activity, my solution was to keep a reference to the fragment in the activity e.g Fragment myfragment = null. And then use the following code in MyFragment:
public void onAttach(Activity activity) {
super.onAttach(activity);
((TestActivity)activity).contentFragment = this;
}
Had a similar issue. Basically if the ViewPager just has couple of fragments, then store references to them in current activity. DO NOT call pagerAdapter's getItem() because it creates a new fragment and it is not attached to any activity and that's why we see "Activity has been destroyed" exception. If you don't want to keep fragment references, then you can use findViewWithTag() method to get Fragment object.
Committing transactions in OnPostResume callback fixed the issue for me. Thanks to following blogpost
http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
#Override
protected void onPostResume() {
super.onPostResume();
// Commit your transactions here.
}
I had this problem with nested fragments and none of the stackoverflow solutions worked for me. Just it seems, that there is a bug with support library, when dismissed fragments still store pointers to previous activity (so getFragmentManager() just returns null, because it is called on already destroyed activity), that's why you need to manage pointers yourself. I ended up with a following solution:1. In the first level fragment I was saving pointer to the activity in the method
public void onAttach(Activity activity) {
super.onAttach(activity);
parentActivity = activity; // parentActivity is static variable
}
2. In the activity which handles fragments I ended up with this code:
private void launchFragment(Fragment fragment, Activity parent) {
FragmentTransaction transaction;
if(parent == null)
transaction = mFragmentManager.beginTransaction();
else // for nested child fragments, workaround for Android parent pointer bug
transaction = parent.getFragmentManager().beginTransaction();
transaction.replace(R.id.container, fragment);
transaction.addToBackStack(null);
transaction.commit();
}
You should pass parentActivity of FIRST level fragment only when you are calling SECOND level (nested) fragments, as it seems that this bug is only with nested ones after you bring your app from foreground.
Related
I'm making an Android App, that uses BottomNavigationViewEx to have a Bottom Navigation widget with 5 sections/fragments, I manage them using a viewpager, but one of this fragment (fragment #3) also uses a tab layout to nest another 2 fragments and I need to keep the selected tab when the user navigates to other fragment using the BottomNavigation icons.
The problem is that I need save the state of the fragment #3 (juts to keep it simple, I call them in this post fragment #), that is the fragment that holds the tablayout.
Inside fragment #3 I'm calling:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("currentDirectoryFragmentId",tabLayout!!.selectedTabPosition)
}
but the method is never being called, and makes sense, because I really never destroy the parent activity, but onDestroy() is being called inside each fragment correctly.
So, How can I save the state of a fragment when the user navigates between fragments that are children of a same activity?
As stated in the comments. You can accomplish this by using a variable inside the parent activity and referring to and setting this variable inside the fragments' onPause() & onResume() methods.
Inside Parent Activity
public static int position = -1;
Inside Child Fragment
#Override
public void onPause() {
super.onPause();
MainActivity.position = viewPager.getCurrentItem();
}
#Override
public void onResume() {
viewPager.setCurrentItem(MainActivity.position);
super.onResume();
}
I've been researching this topic but so far no luck. Basically I'm replacing a fragment (A) with another one (B) using FragmentTransaction.replace. In this other fragment (B) I have a 'Cancel' button in the toolbar which when pressed pops back to the previous transaction (A) by calling getActivity().getSupportFragmentManager().popBackStackImmediate().
The problem is I need to update the Activity toolbar to display a different title when I'm showing fragment A and fragment B. I can't seem to find a method which gets called in fragment A whenever I go from A -> B -> A to inform me that it is visible again. The idea is to set the toolbar title in this callback which I cannot seem to find.
Can anyone point me in the right direction please?
Cheers.
Edit:
Method I call to replace the fragment with another one is as follows:
public static void replaceFragment(FragmentActivity parentActivity, int fragmentToReplaceId, Fragment withFragment, Integer enterAnim, Integer exitAnim)
{
FragmentManager fragmentManager;
FragmentTransaction transaction;
fragmentManager = parentActivity.getSupportFragmentManager();
transaction = fragmentManager.beginTransaction();
if ( (null != enterAnim) &&
(null != exitAnim) )
{
transaction.setCustomAnimations(enterAnim, exitAnim);
}
transaction.replace(fragmentToReplaceId, withFragment);
transaction.addToBackStack(null);
transaction.commit();
}
You can inform by overriding method onResume() in fragment and sending the message to activity or changing the Toolbar directly.
#Override
public void onResume() {
super.onResume();
((AppCompatActivity) getActivity()).getSupportActionBar().setTitle("Title");
}
In one activity, when replace A ---> B (A and B both are fragments), can use this call-back:
#Override
public void onAttachFragment(Fragment fragment) {
}
Solved by creating a simple static method in fragment A as follows:
public static void updateActivityTitle(FragmentActivity activity)
{
activity.setTitle(kActivityTitle);
}
Then I'm calling this method in fragment B as follows:
// cancel button has been pressed
private void cancel()
{
INV_CustomersFragment.updateActivityTitle(getActivity());
getActivity().getSupportFragmentManager().popBackStackImmediate();
}
Not ideal but it gets the job done. Any other solution involving a proper callback would be better
Update:
A better solution is the one described by #Rani at Fragment's onResume() not called when popped from backstack. This is more elegant and more maintainable, in fact I implemented this solution in my project. Compared to an equivalent solution for iOS this is still messy if you ask me, still seems the way to go.
Given an Activity that acts as a Home page (it never closes) that launches various fragments, how to know when the Activity is visible to the user?
From what I have observed, when I open a fragment the lifecycle for the Activity never changes, onPause() is not called. And when I close the fragment, onResume() is not called on my Activity.
Here is how I am starting my fragments, I am using this method and passing the fragment I want to launch to it.
public void addFragment(int containerId, Fragment fragment, boolean addToBackStack) {
// Check if the fragment has been added already. If so, then
// don't add the fragment.
Fragment temp = mFragmentManager.findFragmentByTag(fragment.getClass().getName());
if(temp != null && temp.isAdded()) {
return;
}
FragmentTransaction ft = mFragmentManager.beginTransaction();
ft.add(containerId, fragment, fragment.getClass().getName());
if(addToBackStack)
ft.addToBackStack(null);
ft.commit();
}
What is the methodology for indicating that my Activity is visible again? Thanks in advance!
in the oncreate method of your home activity, call
mFragmentManager.addOnBackStackChangedListener(this) ;
and then define
#Override
public void onBackStackChanged() {
int backStackCount = mFragmentManager.getBackStackEntryCount();
if(backStackCount == 0) {} //back to home screen
}
Your Activity is always Visible even if thousand Fragments are showing at the same time, for the sake of understanding Fragments are just Custom-Views, and the Fragment gives a helping hand in handling your View, so onPause() on your activity does not need to called when a Fragment dies or is born,just like inflating a View.
Just like Sir #Tim Mutton said, you need to check your BackStack to know if you are back, or you can use the ViewGroup method ViewGroup.indexOfChild(View child) - this method will an int of value getChildCount()-1 which means its on top of its fellow sibblings..
Hope it helps
I have an activity running a support viewPager that consists of fragments (the support library variant), which themselves consist of one of three possible fragments. Given that the Android OS destroys and recreates activities on configuration changes (notably screen rotation), I have decided to retain the middle fragments as they run AsyncTasks. The children also may be running other threads so they need to be retained as well. My immediate concern is that:
1) Although the fragments in the viewPager have their onDetach() method called, the children of those fragments never reach onStop(), onDestroy() or onDetach(). Regardless of whether their instances are being retained or not, surely onDetach() should be called since the activity is destroyed.
2) Despite never being stopped, the references to the child fragments are lost; when the activity is recreated and the middle fragments are reattached they fail to find the children using getChildFragmentManager().findFragmentByTag(key).
EDIT - some relevant code from the middle fragments that are run by the viewpager:
#Override
public void onActivityCreated (Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if(cachedLocation==null) {
new WaitForLocation().execute(mCallback);
}
else {
swapFragment(LTAG);
}
}
The above is calling swapFragment correctly after recreating, and proves that the middle fragment is retained. However:
public void swapFragment(String key) {
//use passed key to organise displayed fragment in shell
FragmentManager childFragMngr = getChildFragmentManager();
'childFragMngr' does not contain any fragments - mAdded and mActive are both null in the debugger at this point, which is strange because the onStop() and onDetach() methods are never touched for the children.
if(childFragMngr.findFragmentByTag(key)==null) {
Fragment mFragment = null;
if(key.equals(listFragmentTag)) {
//instantiate a list fragment
mFragment = createListFragment();
}
else if(key.equals(detailFragmentTag)) {
//instantiate a detail fragment
mFragment = createDetailFragment();
}
}
if(mFragment!=null){
getChildFragmentManager().beginTransaction().replace(R.id.subfragment_shell, mFragment, key)
.addToBackStack(null)
.commit();
}
}
}
Any help would be greatly appreciated.
My Android application has an ActionBar that changes which Fragment occupies a certain FrameLayout. I am trying to use onSaveInstanceState to save the state of a Fragment when the tab is changed, so that it can be recovered in onCreateView.
The problem is, onSaveInstanceState is never called. The Fragment's onDestroyView and onCreateView methods are called, but the Bundle supplied to onCreateView remains null.
Can someone please explain to me when onSaveInstanceState is actually called, how I can make sure it gets called when switching tabs, or the best practice for saving and restoring the state of a Fragment when it is detached and re-attached?
Fragment:
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.event_log, container, false);
// Retrieve saved state
if (savedInstanceState != null){
System.out.println("log retrieved");
} else {
System.out.println("log null");
}
return view;
}
#Override
public void onSaveInstanceState(Bundle outState) {
System.out.println("log saved");
super.onSaveInstanceState(outState);
// more code
}
Activity:
/**
* Detach the current Fragment, because another one is being attached.
*/
#Override
public void onTabUnselected(Tab tab, FragmentTransaction ft) {
if (tab.getText().equals(getString(R.string.tab_events))){
if (frEventLog != null) {
ft.detach(frEventLog);
}
}
Fragment#onSaveInstanceState is only called when the Activity hosting the Fragment is destroyed AND there is a chance that you can come back to the same activity AND the fragment is still added to the FragmentManager. The most common case would be screen rotation.
I think your Fragment will also need to do setRetainInstance(true) in onCreate for example. Not exactly sure about that point though.
You should also see this method being called when you press the home button for example. That will destroy the activity but you can go back to it by using the task list for example.
If you just detach() the fragment all you need to do to get it back is to ask the FragmentManager for it.
There are two examples you should have a look at:
ActionBar FragmentTabs and TabHost FragmentTabs
The TabHost example uses
ft.add(containerId, fragment, tag);
// later
fragment = mActivity.getSupportFragmentManager().findFragmentByTag(tag);
to find the instances of previously added Fragments, works until you remove() a Fragment
Regarding onCreateView / onDestroyView: That is called once a fragment gets detached because the next time you attach it needs to create a new View. Note that Fragment#onDetached() is not called when you detach() the fragment because it is still attached to the Activity. It is only detached from the view-hierarchy.
There is another nice example on how to retain fragment state / how to use fragments to retain state in Android Training - Caching Bitmaps.
That example is missing a critical line though:
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit(); // << add this
}
return fragment;
}