Android: master/detail flow (dual-pane) using 1 activity
Asked Answered
L

2

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();
    }
}
Lethbridge answered 21/8, 2014 at 15:31 Comment(0)
L
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();
}
Lethbridge answered 17/5, 2015 at 14:36 Comment(0)
D
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(); 
}
Disadvantage answered 21/8, 2014 at 17:32 Comment(18)
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).Lethbridge
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().Lethbridge
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 github.com/android/platform_frameworks_base/blob/master/core/…. Why can't you keep your code the way it is and replace the call at the first line with super.onCreate(null)?Disadvantage
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.Disadvantage
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?Lethbridge
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.Disadvantage
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?Lethbridge
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?Lethbridge
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.Disadvantage
doesn't this also mean that the outState in the MasterFragment won't be restored, not even on smartphones?Lethbridge
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.Disadvantage
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().Lethbridge
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?Lethbridge
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?Disadvantage
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.Lethbridge
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).Disadvantage
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.Lethbridge
By the way, thanks Pierre-Antoine for your thoughts.Lethbridge

© 2022 - 2024 — McMap. All rights reserved.