UPDATE the issue was that I was using getFragmentManager() througout, instead of using getChildFragmentManager() on fragments with sub-fragments.
The question remains: How would I handle deeply nested fragments in API < 17?
Original Question
My layout structure is as follows.
Main Activity with layout main.xml has a permanent retained fragment verytop, and in a FrameLayout, either fragment A (a.xml) or fragment B (b.xml) is shown. You can switch between A and B by clicking a button (In abottom_inner there is a button which will bring up B, and in b there is a button that will bring back A again.)
As long as I don't rotate, everything works fine. Also, if I stay with fragment A (don't click the button), and rotate, it also works fine. But if I swich to B, back to A again, and then rotate, abottom_inner is invisible.
This is how it looks and startup (portrait mode, only upper portion shown)
After pressing button "show B":
After pressing "show A", it looks as in first screen shot again. Then, after rotating to landscape, I get this
here is the logcat output
***(start)
Main: MAIN ONCREATE
Main: adding A (happens only at startup)
A: onCreateView
A: added atop
A: added abottom
ATop: onCreateView
ABottom: onCreateView
ABottom: added aBottomInner
ABottomInner: onCreateView
ABottomInner: a bottom inner button clicked
***(switch to B)
Main: replacing A with B
B: onCreateView
B: b button clicked
***(switch back to A)
Main: replacing B with A
A: onCreateView
A: added atop
A: added abottom
ATop: onCreateView
ABottom: onCreateView
ABottom: added aBottomInner
ABottomInner: onCreateView
***(rotate to landscape)
Main: MAIN ONCREATE
Main: content exists
A: onCreateView
A: atop already exists
A: abottom already exists
ABottomInner: onCreateView
ABottom: onCreateView
ABottom: aBottomInner already exists
ATop: onCreateView
Looking at logcat, my guess for the reason of the behaviour is the order in which the onCreateView method is called for each of the child fragments. When it works (at startup, and after button click), the onCreateView of ABottomInner is called AFTER the onCreateView of ABottom. And when it doesn't (after rotate, IF you have clicked the buttons before), the order is reversed. So my guess is that in the case of reversed order, ABottomInner becomes "orphaned" - it relies on ABottom's onCreateView being called before, and if that is not so, it can't attach itself properly. Can anyone confirm or refute my guess? Also, are there any rules about the order in which the onCreateView methods are called during rotate, or is that just random? It would appear so, because if you rotate right after startup, the order is not reversed, and the fragment stays visible.
I have a dirty ugly workaround, which is activated if you check the CheckBox. Then, ABottomInner will be recreated, even if it already exists. Then, it works as it is supposed to. ABottomInner's onCreateView will then be called twice. I can't imagine this being the proper way of doing it. What's the proper way of making sure abottom_inner does not disappear?
Here is complete code.
main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:gravity="center_horizontal"
android:orientation="vertical" >
<fragment
android:id="@+id/VeryTopFragment"
android:name="com.example.nestedfrags.VeryTop"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@+id/contentFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
verytop.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/lime"
>
<TextView
android:id="@+id/veryTopTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" ??? "
android:layout_gravity="center_horizontal"
/>
</LinearLayout>
a.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/aTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/orange"
/>
<FrameLayout
android:id="@+id/aBottom"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/navy"
/>
</LinearLayout>
atop.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/yellow"
>
<TextView
android:id="@+id/atopTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="---"
/>
</LinearLayout>
abottom.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/iamMain"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@color/silver">
<CheckBox
android:id="@+id/checkBox1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CheckBox" />
<FrameLayout
android:id="@+id/abottomFL"
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
</FrameLayout>
</FrameLayout>
abottom_inner.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/white"
>
<TextView
android:id="@+id/aBottomInnerTV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textColor="@color/black"
android:text=" ??? "
/>
<Button
android:id="@+id/aBotInnerButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="show B" />
</LinearLayout>
b.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/bButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="show A" />
</LinearLayout>
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="white">#FFFFFF</color>
<color name="yellow">#FFFF00</color>
<color name="silver">#C0C0C0</color>
<color name="lime">#00FF00</color>
<color name="navy">#000080</color>
<color name="black">#000000</color>
<color name="orange">#F7931E</color>
</resources>
MainActivity.java
package com.example.nestedfrags;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends Activity {
private static final String TAG = "Main";
public VeryTop veryTop;
Fragment contentFragment;
public void showB(){
FragmentManager fm = getFragmentManager();
contentFragment = fm.findFragmentById(R.id.contentFragment);
FragmentTransaction ft = fm.beginTransaction();
B b = new B();
// wouldn't it be nice if android were smart enough to remove "dependents" by itself?!
ft.remove(fm.findFragmentById(R.id.aTop));
ft.remove(fm.findFragmentById(R.id.aBottom));
ft.remove(fm.findFragmentById(R.id.abottomFL));
if (contentFragment == null){
Log.i(TAG, "adding B");
ft.add(R.id.contentFragment, b, "B").commit();
} else {
Log.i(TAG, "replacing A with B");
ft.replace(R.id.contentFragment, b, "B").commit();
}
}
public void showA(){
FragmentManager fm = getFragmentManager();
contentFragment = fm.findFragmentById(R.id.contentFragment);
FragmentTransaction ft = fm.beginTransaction();
if (contentFragment == null){
Log.i(TAG, "adding A");
ft.add(R.id.contentFragment, new A(), "A").commit();
} else {
Log.i(TAG, "replacing B with A");
ft.replace(R.id.contentFragment, new A(), "A").commit();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, " MAIN ONCREATE ");
setContentView(R.layout.main);
FragmentManager fm = getFragmentManager();
contentFragment = fm.findFragmentById(R.id.contentFragment);
veryTop = (VeryTop)fm.findFragmentById(R.id.VeryTopFragment);
if (contentFragment == null){
Log.i(TAG, "adding A (happens only at startup)");
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.contentFragment, new A(), "A").commit();
} else {
Log.i(TAG, "content exists");
}
}
}
VeryTop.java
package com.example.nestedfrags;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class VeryTop extends Fragment {
public boolean forceInnerRecreation = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
public void updateTextView(View v){
TextView tv = (TextView)v.findViewById(R.id.veryTopTV);
tv.setText(" very top - forceInnerRecreation is: " + forceInnerRecreation);
}
public void setForceInnerRecreation(boolean value){
forceInnerRecreation = value;
updateTextView(getView());
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.verytop, container, false);
updateTextView(v);
return v;
}
}
A.java
package com.example.nestedfrags;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class A extends Fragment {
private static final String TAG = "A";
private void incarnateTop(View v, FragmentManager fm){
int layoutId = R.id.aTop;
ATop fragment = (ATop)fm.findFragmentById(layoutId);
boolean fragmentWasNull = false;
if (fragment == null){
fragment = new ATop();
fragmentWasNull = true;
}
fragment.text = " == A TOP == ";
if (fragmentWasNull){
FragmentTransaction ft = fm.beginTransaction();
ft.add(layoutId, fragment, "atop").commit();
Log.i(TAG, "added atop");
} else {
Log.i(TAG, "atop already exists");
}
}
private void incarnateBottom(View v, FragmentManager fm){
int layoutId = R.id.aBottom;
ABottom fragment = (ABottom)fm.findFragmentById(layoutId);
boolean fragmentWasNull = false;
if (fragment == null){
fragment = new ABottom();
fragmentWasNull = true;
}
if (fragmentWasNull){
FragmentTransaction ft = fm.beginTransaction();
ft.add(layoutId, fragment, "abottom").commit();
Log.i(TAG, "added abottom");
} else {
Log.i(TAG, "abottom already exists");
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.a, container, false);
Log.i(TAG, "onCreateView");
FragmentManager fm = getFragmentManager();
incarnateTop(v, fm);
incarnateBottom(v, fm);
return v;
}
}
ATop.java
package com.example.nestedfrags;
import android.app.Fragment;
import android.app.FragmentManager;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class ATop extends Fragment {
private static final String TAG = "ATop";
String text = "invalid";
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.atop, container, false);
Log.i(TAG, "onCreateView");
FragmentManager fm = getFragmentManager();
TextView tv = (TextView)v.findViewById(R.id.atopTV);
tv.setText(text);
return v;
}
}
ABottom.java
package com.example.nestedfrags;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.FrameLayout;
public class ABottom extends Fragment {
private static final String TAG = "ABottom";
private void incarnateInner(View v, FragmentManager fm){
int layoutId = R.id.abottomFL;
FrameLayout container = (FrameLayout)v.findViewById(layoutId);
container.setPadding(0, 200, 0, 0);
ABottomInner fragment = (ABottomInner)fm.findFragmentById(layoutId);
boolean fragmentWasNull = false;
if (fragment == null){
fragment = new ABottomInner();
fragmentWasNull = true;
}
fragment.text = " -- a bottom inner -- ";
if (fragmentWasNull){
FragmentTransaction ft = fm.beginTransaction();
ft.add(layoutId, fragment, "aBottomInner").commit();
Log.i(TAG, "added aBottomInner");
} else {
if (forceInnerRecreation()){
FragmentTransaction ft = fm.beginTransaction();
fragment = new ABottomInner();
fragment.text = " using brute workaround ";
ft.replace(layoutId, fragment, "aBottomInner").commit();
Log.i(TAG, "putting in fresh copy of aBottomInner");
} else {
Log.i(TAG, "aBottomInner already exists");
}
}
}
private boolean forceInnerRecreation(){
MainActivity main = (MainActivity)getActivity();
return main.veryTop.forceInnerRecreation;
}
private void setForceInnerRecreation(boolean value){
MainActivity main = (MainActivity)getActivity();
main.veryTop.setForceInnerRecreation(value);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.abottom, container, false);
Log.i(TAG, "onCreateView");
FragmentManager fm = getFragmentManager();
CheckBox cb = (CheckBox)v.findViewById(R.id.checkBox1);
cb.setChecked(forceInnerRecreation());
OnCheckedChangeListener cbListener = new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
setForceInnerRecreation(isChecked);
if (isChecked){
Log.i(TAG, "setting brute workaround ON");
} else {
Log.i(TAG, "setting brute workaround OFF");
}
}
};
cb.setOnCheckedChangeListener(cbListener);
incarnateInner(v, fm);
return v;
}
}
ABottomInner.java
package com.example.nestedfrags;
import android.app.Fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
public class ABottomInner extends Fragment {
private static final String TAG = "ABottomInner";
String text = "invalid";
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.abottom_inner, container, false);
Log.i(TAG, "onCreateView");
TextView tv = (TextView)v.findViewById(R.id.aBottomInnerTV);
tv.setText(text);
Button btn = (Button)v.findViewById(R.id.aBotInnerButton);
OnClickListener listener = new OnClickListener() {
@Override
public void onClick(View v) {
Log.i(TAG, "a bottom inner button clicked");
MainActivity main = (MainActivity)getActivity();
main.showB();
}
};
btn.setOnClickListener(listener);
return v;
}
}
B.java
package com.example.nestedfrags;
import android.app.Fragment;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.Button;
public class B extends Fragment {
private static final String TAG = "B";
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
Log.i(TAG, "onCreateView");
View v = inflater.inflate(R.layout.b, container, false);
Button bButton = (Button)v.findViewById(R.id.bButton);
OnClickListener listener = new OnClickListener(){
@Override
public void onClick(View v) {
Log.i(TAG, "b button clicked");
MainActivity main = (MainActivity)getActivity();
main.showA();
}
};
bButton.setOnClickListener(listener);
return v;
}
}
setRetainInstance(true)
?