-1

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.

fragment structure

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)

a

After pressing button "show B":

b

After pressing "show A", it looks as in first screen shot again. Then, after rotating to landscape, I get this

invisible fragment

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;
    }
}
5
  • Call setRetainInstance(true) ?
    – Ajay S
    Commented Jan 16, 2015 at 13:41
  • @TGMCians: I just tried out setRetainInstance(true) on ABottomInner. Result: If you first click "show B" button, then "show A" button, then rotate, the app crashes with a NullPointerException. Commented Jan 16, 2015 at 13:49
  • 1
    Actually You are presenting Fragment inside another fragment . So you should use getChildFragmentManager() for making transactions . other wise it will lead to other problems.
    – Krish
    Commented Jan 16, 2015 at 19:18
  • 1
    Eg: For adding/replacing ABottom inside A use getchildFragmentManager()
    – Krish
    Commented Jan 16, 2015 at 19:25
  • @Krish: That's it! With getChildFragmentManager() it works perfectly, and I could even remove the annoying lines of code where the MainActivity removes it's grandchildren manually (the grandchildren which weren't really grandchildren from Android's point of view, because I did not use getChildFragmentManager().) - Make that an answer, and I'll accept it. Commented Jan 16, 2015 at 22:01

2 Answers 2

1

Actually You are presenting Fragment inside another fragment . So you should use getChildFragmentManager() for making transactions . other wise it will lead to other problems.

Eg: For adding/replacing ABottom inside A use getchildFragmentManager()

-1

thanks to comment from Krish, I figured it out.

[1] in all fragments, replace getFragmentManager by getChildFragmentManager.

[2] remove the 3 lines of the form ft.remove(fm.findFragmentById(R.id. ... )); from MainActivity

then it works.

Not the answer you're looking for? Browse other questions tagged or ask your own question.