8

As reported by the Android guide, dual-pane can be achieved in two ways:

  1. Multiple fragments, one activity
  2. Multiple fragments, multiple activities

I am using the first case (the Android guide only explains the second case).

This is what happens on 7" tablets:

  • rotating from landscape to portrait: only the single-pane fragment gets recreated
  • rotating from portrait to landscape: all 3 fragments (single-pane, dual-pane-master, dual-pane-detail) get recreated

Question: why is the single-pane fragment (which I create programmatically, but using a FrameLayout defined in the layout as the container) get recreated on dual pane?

I am reporting below my implementation:

/layout/activity_main.xml:

<FrameLayout
    android:id="@+id/single_pane"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

/layout-w900dp/activity_main.xml:

<LinearLayout
    android:id="@+id/dual_pane"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment class="com.example.MasterFragment"
        android:id="@+id/master_dual"
        android:tag="MASTER_FRAGMENT_DUAL_PANE"
        android:layout_width="@dimen/master_frag_width"
        android:layout_height="match_parent"/>
    <fragment class="com.example.DetailFragment"
        android:id="@+id/detail_dual"
        android:tag="DETAIL_FRAGMENT_DUAL_PANE"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

This is the onCreate in the main activity:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mDualPane = findViewById(R.id.dual_pane)!=null;

    FragmentManager fm = getFragmentManager();
    if (savedInstanceState==null) {
        // this is a non-UI fragment I am using for data processing purposes
        fm.beginTransaction().add(new NonUiFragment(), DATA_FRAGMENT).commit();
    }
    if (!mDualPane && fm.findFragmentById(R.id.single_pane)==null) {
        fm.beginTransaction().add(R.id.single_pane, new MasterFragment(), MASTER_FRAGMENT_SINGLE_PANE).commit();
    }
}
Daniele B
  • 19,801
  • 29
  • 115
  • 173

2 Answers2

5

I found it's much better to add the fragments in the code also for the dual pane.

So, instead of using the <fragment>, also use the <FrameLayout> also for the dual-pane XML.

/layout-w900dp/activity_main.xml:

<LinearLayout
    android:id="@+id/dual_pane"
    android:orientation="horizontal"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <FrameLayout
        android:id="@+id/master_dual"
        android:layout_width="@dimen/master_frag_width"
        android:layout_height="match_parent"/>
    <FrameLayout
        android:id="@+id/detail_dual"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

In this way, you can use just one instance of the masterFragment and of the DetailFragment, so you don't fall into the problem of having multiple instances of the same fragment.

In order to do this, in the OnCreate you need to add the fragments to the container, detaching from the old container:

    mDualPane = findViewById(R.id.dual_pane)!=null;

    if (savedInstanceState!=null) {
        mLastSinglePaneFragment = savedInstanceState.getString("lastSinglePaneFragment");
    }

    FragmentManager fm = getSupportFragmentManager();

    if (!mDualPane && fm.findFragmentById(R.id.single_pane)==null) {
        MasterFragment masterFragment = getDetatchedMasterFragment(false);
        fm.beginTransaction().add(R.id.single_pane, masterFragment, MASTER_FRAGMENT).commit();
        if (mLastSinglePaneFragment==DETAIL_FRAGMENT) {
            openSinglePaneDetailFragment();
        }
    }
    if (mDualPane && fm.findFragmentById(R.id.master_dual)==null) {
        MasterFragment masterFragment = getDetatchedMasterFragment(true);
        fm.beginTransaction().add(R.id.master_dual, masterFragment, MASTER_FRAGMENT).commit();
    }
    if (mDualPane && fm.findFragmentById(R.id.detail_dual)==null) {
        DetailFragment detailFragment = getDetatchedDetailFragment();
        fm.beginTransaction().add(R.id.detail_dual, detailFragment, DETAIL_FRAGMENT).commit();
    }

using these functions:

public static final String MASTER_FRAGMENT = "MASTER_FRAGMENT";
public static final String DETAIL_FRAGMENT = "DETAIL_FRAGMENT";

private MasterFragment getDetatchedMasterFragment(boolean popBackStack) {
    FragmentManager fm = getSupportFragmentManager();
    MasterFragment masterFragment = getSupportFragmentManager().findFragmentByTag(MASTER_FRAGMENT);
    if (masterFragment == null) {
        masterFragment = new MasterFragment();
    } else {
        if (popBackStack) {
            fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        }
        fm.beginTransaction().remove(masterFragment).commit();
        fm.executePendingTransactions();
    }
    return masterFragment;
}

private DetailFragment getDetatchedDetailFragment() {
    FragmentManager fm = getSupportFragmentManager();
    DetailFragment detailFragment = getSupportFragmentManager().findFragmentByTag(DETAIL_FRAGMENT);
    if (detailFragment == null) {
        detailFragment = new DetailFragment();
    } else {
        fm.beginTransaction().remove(detailFragment).commit();
        fm.executePendingTransactions();
    }
    return detailFragment;
}

private void openSinglePaneDetailFragment() {
    FragmentManager fm = getSupportFragmentManager();
    fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
    DetailFragment detailFragment = getDetatchedDetailFragment();
    FragmentTransaction fragmentTransaction = fm.beginTransaction();
    fragmentTransaction.replace(R.id.single_pane, detailFragment, DETAIL_FRAGMENT);
    fragmentTransaction.addToBackStack(null);
    fragmentTransaction.commit();
}
Daniele B
  • 19,801
  • 29
  • 115
  • 173
0

When you rotate, the currently active fragments will be saved by the FragmentManager and used to recreate fragments automatically when the new Activity is created. You can prevent recreation by not passing the savedInstanceState to the super method. E.g. super.onCreate(null);.

Alternatively if you need to restore state using the FragmentActivity.onCreate(savedInstanceState) method (which calls FragmentManager.restoreAllState() —see https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/FragmentManager.java#L1759), you can lookup your fragment tag and remove it manually in your onCreate. This is the case since you have a non-ui Fragment you want to restore. The restoration of retained fragments also depends on the call to FragmentActivity.onCreate(savedInstanceState) with saveInstanceState != null.

The recreation happens because usually you want to keep the active fragment around (and possibly add a second detail pane in the case of tablets).

if (mDualPane) {
    Fragment singlePane = getFragmentManager().findFragmen‌​tByTag(MASTER_FRAGMENT_SINGLE_PANE);
    if (singlePane != null)
        getFragmentManager().beginTransaction().remove(fragment).commit(); 
}
Pierre-Antoine LaFayette
  • 24,222
  • 8
  • 54
  • 58
  • Hi Pierre-Antoine, I realized that all 3 fragments are kept by the fragment manager. But I don't understand why the single-pane fragment should get recreated on landspace (despite its container not being in the layout), while instead the dual-pane fragments correctly don't get recreated on portrait (as they are not in the layout). – Daniele B Aug 21 '14 at 18:28
  • I can't find a way to implement what you say. I am detecting if it's a dual-pane or a single-pane based on the `R.id.dual_pane` layout. I only have this information after `setContentView()`, which I guess should be after `super.OnCreate()`. – Daniele B Aug 21 '14 at 18:44
  • It has nothing to do with whether the container exists in your content view. The FragmentManager creates new instances of all Fragments that were active after an orientation change causes the Fragments to be destroyed. In FragmentActivity.onCreate(), there is a call to mFragments.restoreAllState() which will instantiate any previously active Fragments in the bundle. See https://github.com/android/platform_frameworks_base/blob/master/core/java/android/app/FragmentManager.java#L1759. Why can't you keep your code the way it is and replace the call at the first line with super.onCreate(null)? – Pierre-Antoine LaFayette Aug 21 '14 at 19:29
  • You aren't using the savedInstanceState and you are replacing the active Fragment(s) each orientation change, so there's no state you need restored by the parent Activity. – Pierre-Antoine LaFayette Aug 21 '14 at 19:32
  • I am actually also using a non-UI fragment. I just added it to the code in the question. I guess I need to restore the activity state for that, right? – Daniele B Aug 21 '14 at 19:51
  • I was going to say you can call setRetainInstance(true) on the no-ui Fragment but it looks like that depends on the call to restoreAllState as well. So in that case you might need to manually remove the Fragment when you detect dual pane by looking up the fragment tag and making a remove() transaction. – Pierre-Antoine LaFayette Aug 21 '14 at 19:59
  • You say that "The FragmentManager creates new instances of all Fragments that were active after an orientation change causes the Fragments to be destroyed". So why when rotating from landscape to portrait, the onCreate doesn't get called on the dual-pane fragments? – Daniele B Aug 21 '14 at 20:06
  • I just realized I am using `onSaveInstanceState(Bundle outState)` in the MasterFragment to restore some values. If I set `super.onCreate(null)` on the Activity, I think that outState can't be retrieved :-( I would need to move those data to the non-UI-fragment? – Daniele B Aug 21 '14 at 20:07
  • Your best bet now is to just call super.onCreate(saveInstanceState) and then call `getFragmentManager().beginTransaction().remove(getFragmentManager().findFragmentByTag(MASTER_FRAGMENT_SINGLE_PANE)).commit();` I.e. let the single pane fragment get instantiated then remove it when you detect you are in dual pane mode. – Pierre-Antoine LaFayette Aug 21 '14 at 20:09
  • doesn't this also mean that the outState in the MasterFragment won't be restored, not even on smartphones? – Daniele B Aug 21 '14 at 20:14
  • See my updated answer. For your case I don't recommend calling super.onCreate(null) since you need to restore state. You just need to look up the single pane fragment if you are switching to dual pane and remove it. – Pierre-Antoine LaFayette Aug 21 '14 at 20:16
  • I have tried to do what you said. Removing (and committing!) the single pane fragment after `setContentView()`, but the fragment still seems to exist even after commit, inside the `Activity.onCreate()`. And even the `Fragment.OnCreate()` gets called. I have checked to have done this correctly. It seems the fragment transactions get committed only after the OnCreate(). – Daniele B Aug 21 '14 at 20:43
  • Ideally, I am looking for the behaviour of the single pane fragment to be similar to the dual pane fragments. That is: always staying in the fragment manager, without the need of being explicitely removed, but at the same time not to get recreated when it's not needed. Is this impossible to achieve? Why do the dual pane fragments (which are not removed!) don't get recreated on single pane, but the single pane fragment gets recreated on dual pane? – Daniele B Aug 21 '14 at 20:50
  • Thats interesting it may be because you have fragments defined in your layouts for the dual pane mode. I don't usually define fragments like that so I don't know for sure. Have you tried defining the single-pane in your portrait mode layout file instead of in code? – Pierre-Antoine LaFayette Aug 22 '14 at 03:32
  • I can't define the single-pane fragment in the layout file, because it needs to be replaceable (as I was saying earlier in the question, I am using the "multiple fragments, one activity" approach). Android doesn't allow to replace fragments defined in the layout. That's why I am only defining the FrameLayout container in the layout. – Daniele B Aug 22 '14 at 03:36
  • Then I think you are stuck with letting the FragmentManager recreate the instance and removing it. Sometimes the FragmentManager tries to be too smart and doesn't give you enough fine grain control. Hopefully they'll address it in a future release (along the lines of the ListView vs the new RecyclerView implementation). – Pierre-Antoine LaFayette Aug 22 '14 at 14:56
  • At this point, I am not sure it has even a sense to remove it. I am thinking to always keep that single-pane fragment. This could actually even have some benefit, as if you rotate back and forward between single-pane amd dual-pane, the single-pane would remember the last fragment. I am trying to understand which drawback this could have. At the moment, I haven't found. I guess it depends on your overall design. In my case, so far I haven't experience any major issues by allowing the single-pane fragment to recreate itself. – Daniele B Aug 22 '14 at 16:49
  • By the way, thanks Pierre-Antoine for your thoughts. – Daniele B Aug 22 '14 at 16:51