5

Info: I have a 2 pane layout (2 child Fragments) inside a ParentFragment, which, of course, is inside a FragmentActivity. I have setRetainInstance(true) on the ParentFragment. On orientation change, the left child fragment doesn't get destroyed (onCreate() doesn't get called), which is normal (because of the parent retaining its instance).

Problem: On orientation change, the right fragment gets destroyed (onCreate() gets called). Why the hell is the right fragment destroyed and the left one isn't ?

EDIT: If I remove setRetainInstance(true), then the left fragment's onCreate() gets called twice (lol wtf) and the right fragment's onCreate() gets called once. So this isn't good either...

Code below for the ParentFragment:

@Override
public void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    this.setRetainInstance(true);
}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState)
{
    View view = inflater.inflate(R.layout.fragment_schedule, container, false);
    setHasOptionsMenu(true);


    if (getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_left) == null || 
            !getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_left).isInLayout())
    {
        if (mPresentationsListFragment == null)
            mPresentationsListFragment = PresentationsListFragment.newInstance(PresentationsListFragment.TYPE_SCHEDULE, mScheduleDate);
        getChildFragmentManager().beginTransaction()
                                     .replace(R.id.fragment_schedule_framelayout_left, mPresentationsListFragment)
                                     .commit();
    }
    mPresentationsListFragment.setOnPresentationClickListener(this);


    return view;
}


@Override
    public void onPresentationClick(int id)
    {
        if (Application.isDeviceTablet(getActivity()))
        {
            if (getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_right) == null)
            {
                if (mPresentationDetailFragment == null)
                    mPresentationDetailFragment = PresentationDetailFragment.newInstance(id);
                else
                    mPresentationDetailFragment.loadPresentation(id);
                getChildFragmentManager().beginTransaction()
                                           .replace(R.id.fragment_schedule_framelayout_right, mPresentationDetailFragment)
                                           .commit();
            }
            else
                mPresentationDetailFragment.loadPresentation(id);
        }
        else
        {
            Intent presentationDetailIntent = new Intent(getActivity(), PresentationDetailActivity.class);
            presentationDetailIntent.putExtra(PresentationDetailActivity.KEY_PRESENTATION_ID, id);
            startActivity(presentationDetailIntent);
        }
    }

LE Solution: Thanks a lot to antonyt , the answer is below. The only changes needed to pe performed reside inside onCreateView() of the parent Fragment.

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState)
{
    View view = inflater.inflate(R.layout.fragment_schedule, container, false);
    setHasOptionsMenu(true);


    if (getChildFragmentManager().findFragmentById(R.id.fragment_presentations_framelayout_left) == null)
    {
        mPresentationsListFragment = PresentationsListFragment.newInstance();
        mPresentationsListFragment.setOnPresentationClickListener(this);
        getChildFragmentManager().beginTransaction()
                .add(R.id.fragment_presentations_framelayout_left, mPresentationsListFragment)
                .commit();
    }


    return view;
}
Blo
  • 11,903
  • 5
  • 45
  • 99
Bogdan Zurac
  • 6,348
  • 11
  • 48
  • 96
  • As for me, calling `ParentFragment.setRetainState(true)` doesn't make retain state of it's child fragments, not depending on the fact whether this frag is set from the layout or dynamically in the `ParentFragment.onCreateView()`. – riwnodennyk Mar 05 '13 at 15:06
  • I'm sorry, I didn't understand what you meant. None of the fragments are set in XMl. The XML layout file contains only *FrameLayouts*. – Bogdan Zurac Mar 05 '13 at 18:31

1 Answers1

5

From what I understand, if you have setRetainInstance(true) on the parent fragment with the above code, your left fragment should be recreated but your right fragment should not be, when changing orientation. This is backwards to what you wrote above, but I will explain why this is the case anyway. If you have setRetainInstance(false) on the parent fragment, you indeed should see the left fragment being created twice and the right fragment being created once.

Case 1: setRetainInstance(true)

Your parent fragment will not be destroyed on rotation. However, it will still recreate its views each time (onDestroyView and onCreateView will be called, in that order). In onCreateView you have code to add your left fragment under certain conditions. getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_left) should be non-null, since a fragment was added to that container previously. getChildFragmentManager().findFragmentById(R.id.fragment_schedule_framelayout_left).isInLayout() should be false since only fragments added via XML will cause it to return true. The overall condition is true and so a new instance of your left fragment will be created and it will replace the old one. Your right fragment is only instantiated during a click event and so no special behavior happens.

Summary: Parent fragment remains, new left fragment is created, right fragment remains.

Case 2: setRetainInstance(false)

Your parent fragment is destroyed, and so are the left and right fragments. All three fragments are recreated automatically by Android. Your parent fragment will then get a chance to create its view, and it will create a new instance of the left fragment as per the explanation above. The just-created left fragment will be replaced by this new instance. You will observe that a left fragment will be destroyed and another left fragment will be created. No special behavior happens for the right fragment.

Summary: New parent fragment is created, two new left fragments are created, new right fragment is created.

If you are sure that in the setRetainInstance(true) case, your right fragment is being destroyed and not your left one, please post a sample project to github/etc. that demonstrates this.

Update: Why the right fragment gets removed if you use FragmentTransaction.replace() on the left fragment

Because of the inner conditional, your code will try to replace your left fragment with itself on the same container.

Here is the code snippet from the Android 4.1 source code that handles a replace:

...
case OP_REPLACE: {
    Fragment f = op.fragment;
    if (mManager.mAdded != null) {
        for (int i=0; i<mManager.mAdded.size(); i++) {
            Fragment old = mManager.mAdded.get(i);
            if (FragmentManagerImpl.DEBUG) Log.v(TAG,
                    "OP_REPLACE: adding=" + f + " old=" + old);
            if (f == null || old.mContainerId == f.mContainerId) {
                if (old == f) {
                    op.fragment = f = null;
                } else {
                    if (op.removed == null) {
                        op.removed = new ArrayList<Fragment>();
                    }
                    op.removed.add(old);
                    old.mNextAnim = op.exitAnim;
                    if (mAddToBackStack) {
                        old.mBackStackNesting += 1;
                        if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of "
                                + old + " to " + old.mBackStackNesting);
                    }
                    mManager.removeFragment(old, mTransition, mTransitionStyle);
                }
            }
        }
    }
    if (f != null) {
        f.mNextAnim = op.enterAnim;
        mManager.addFragment(f, false);
    }
} break;
...

If you try to replace the same fragment with itself, there is some code to try and ignore this operation:

if (old == f) {
    op.fragment = f = null;
}

Since f is null, and we are still continuing to iterate through our fragments, this seems to have the side effect of removing every subsequent fragment from the FragmentManager. I don't think this is intentional, but at the very least explains why your right fragment is getting destroyed. Not using replace / not replacing the same fragment with itself can fix your issues.

Interestingly, this was a recent change and did not exist in previous versions of Android. https://github.com/android/platform_frameworks_support/commit/5506618c80a292ac275d8b0c1046b446c7f58836

Bug report: https://code.google.com/p/android/issues/detail?id=43265

antonyt
  • 21,863
  • 9
  • 71
  • 70
  • Case 1, "The overall condition is true and so a new instance of your left fragment will be created". No. You forgot about the inner if on that case. if (mPresentationsListFragment == null) mPresentationsListFragment = PresentationsListFragment.newInstance(PresentationsListFragment.TYPE_SCHEDULE, mScheduleDate); If this *if* wasn't present, then yes, the onCreate() for the left fragment should've kicked in, I totally agree. But unfortunately, the inner if exists. – Bogdan Zurac Jul 26 '13 at 11:55
  • Aha, I think I understood your 2nd case and I also agree with you on that. So for case no. 2 (if I want to use it), I only need to remove the creation of the left fragment from the parent's onCreateView() if savedInstanceState is != null and that should solve it in that case. But my initial questions stands; what about the first case ? – Bogdan Zurac Jul 26 '13 at 12:00
  • I've managed to reproduce it in a sample application. Here is the link http://www.filehostfree.com/?d=51F26D5E1 Everything works fine, until orientation change. There you can see in the logs, parent fragment onCreateView(), left onCreateView(), right onCreateView(), then suddenly, right onDestroy(). So what's up with that onDestroy() call ? – Bogdan Zurac Jul 26 '13 at 12:38
  • Ah yes, I forgot about your second inner conditional statement. In any case, I would remove the outer isInLayout check and the inner conditional. I don't know exactly why the presentation detail fragment gets destroyed, but I can tell you that changing 'replace' to 'add' causes it to work properly (the right fragment does not get destroyed on rotate). I will look more into why this happens. – antonyt Jul 26 '13 at 14:33
  • @Andrew I have updated my answer with what I have been able to find. – antonyt Jul 26 '13 at 15:05
  • Wow. I'm completely impressed by your research and completely stoned by the bug in the framework at the same time. I once said that the Fragment part of the framework is really kinda bugged, but this... oh this is pretty darn annoying. Thanks a lot for your effort. I will check this later today in code if I catch a break. Awesome find btw. – Bogdan Zurac Jul 31 '13 at 06:08
  • If I try to use add instead of replace, it tries to add the fragment, even though it already is added. So it doesn't work. How did it work for you ? LE: Oh, nevermind, I got it. I'll also update the initial question with the corresponding code so that it's also visible for others. Thanks a lot for everything. – Bogdan Zurac Aug 01 '13 at 09:27