19

I have a problem reloading an activity with tabs and fragments when I change the orientation of my device.

Here's the situation:

I have an activity which has 3 tabs in the action bar. Each tab loads a different fragment in a FrameLayout in main view. Everything works fine if I don't change the orientation of the device. But when I do that Android tries to initialize the currently selected fragment twice which produce the following error:

E/AndroidRuntime(2022): Caused by: android.view.InflateException: Binary XML file line #39: Error inflating class fragment

Here's the sequence of steps that produce the error:

  1. I load the activity, select tab nr 2. and change the orientation of the device.
  2. Android destroys the activity and the instance of the fragment loaded by tab nr 2 (from now on, 'Fragment 2'). Then it proceeds to create new instances of the activity and the fragment.
  3. Inside Activity.onCreate() I add the first tab to the action bar. When I do that, this tab gets automatically selected. It may represent a problem in the future, but I don't mind about that now. onTabSelected gets called and a new instance of the first fragment is created and loaded (see code below).
  4. I add all the other tabs without any event being triggered, which is fine.
  5. I call ActionBar.selectTab(myTab) to select Tab nr 2.
  6. onTabUnselected() gets called for the first tab, and then onTabSelected() for the second tab. This sequence replaces the current fragment for an instance of Fragment 2 (see code below).
  7. Next, Fragment.onCreateView() is called on Fragment 2 instance and the fragment layout gets inflated.
  8. Here is the problem. Android Calls onCreate() and then onCreateView() on the fragment instance ONCE AGAIN, which produces the exception when I try to inflate (a second time) the layout.

Obviously the problem is Android is initializing the fragment twice, but I don't know why.

I tried NOT selecting the second tab when I reaload the activity but the second fragment gets initialized anyway and it is not shown (since I didn't select its tab).

I found this question: Android Fragments recreated on orientation change

The user asks basically the same I do, but I don't like the chosen answer (it's only a workaroud). There must be some way to get this working without the android:configChanges trick.

In case it's not clear, what I want to know how whether to prevent the recreation of the fragment or to avoid the double initialization of it. It would be nice to know why is this happening also. :P

Here is the relevant code:

public class MyActivity extends Activity  implements ActionBar.TabListener {

    private static final String TAG_FRAGMENT_1 = "frag1";
    private static final String TAG_FRAGMENT_2 = "frag2";
    private static final String TAG_FRAGMENT_3 = "frag3";

    Fragment frag1;
    Fragment frag2;
    Fragment frag3;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // my_layout contains a FragmentLayout inside
        setContentView(R.layout.my_layout); 

        // Get a reference to the fragments created automatically by Android
        // when reloading the activity
        FragmentManager fm = getFragmentManager();
        this.frag1 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_1);
        this.frag2 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_2);
        this.frag3 = fm.findFragmentByTag(MyActivity.TAG_FRAGMENT_3)


        ActionBar actionBar = getActionBar();

        // snip...

        // This triggers onTabSelected for the first tab
        actionBar.addTab(actionBar.newTab()
                .setText("Tab1").setTabListener(this)
                .setTag(MyActivity.TAG_FRAGMENT_1));

        actionBar.addTab(actionBar.newTab()
                .setText("Tab2").setTabListener(this)
                .setTag(MyActivity.TAG_FRAGMENT_2));
        actionBar.addTab(actionBar.newTab()
                .setText("Tab3").setTabListener(this)
                .setTag(MyActivity.TAG_FRAGMENT_3));

        Tab t = null;
        // here I get a reference to the tab that must be selected
        // snip...

        // This triggers onTabUnselected/onTabSelected
        ab.selectTab(t);

    }

    @Override
    protected void onDestroy() {
        // Not sure if this is necessary
        this.frag1 = null;
        this.frag2 = null;
        this.frag3 = null;
        super.onDestroy();
    }

    @Override  
    public void onTabSelected(Tab tab, FragmentTransaction ft) {  

        Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
        if (curFrag == null) {
            curFrag = createFragmentInstanceForTag(tab.getTag().toString());
            if(curFrag == null) { 
                // snip... 
                return;
            }
        }
        ft.replace(R.id.fragment_container, curFrag, tab.getTag().toString());
    }

    @Override  
    public void onTabUnselected(Tab tab, FragmentTransaction ft) 
    {  
        Fragment curFrag = getFragmentInstanceForTag(tab.getTag().toString());
        if (curFrag == null) {
            // snip... 
            return;
        }

        ft.remove(curFrag);
    }

    private Fragment getFragmentInstanceForTag(String tag) 
    {
        // Returns this.frag1, this.frag2 or this.frag3
        // depending on which tag was passed as parameter
    }

    private Fragment createFragmentInstanceForTag(String tag) 
    {
        // Returns a new instance of the fragment requested by tag
        // and assigns it to this.frag1, this.frag2 or this.frag3
    }
}

The code for the Fragment is irrelevant, it just returns an inflated view on onCreateView() method override.

Community
  • 1
  • 1
Gerardo Contijoch
  • 2,421
  • 5
  • 20
  • 29
  • 1
    Just a suggestion... you're more likely to get answers if you cut down the size of your post :P. Although I certainly appreciate how much detail you've included... I suggest you include some sort of summary that is easily readable and grabs people's attention (somewhere at the beginning of your post). – Alex Lockwood Jun 25 '12 at 16:11
  • Also, using `inline code`, **bold face** (but not too much), and even html headers (

    ) can help organize your post... but don't abuse it too much :P
    – Alex Lockwood Jun 25 '12 at 16:12
  • Need some code to go by, preferably the `onTabSelected` and `onTabUnselected` methods – StuStirling Jun 25 '12 at 16:20
  • Thank you for the suggestions. I thought about the length of the post but I wanted to detail as much as possible because everything seems to be working fine, except for the last part. – Gerardo Contijoch Jun 25 '12 at 16:38

9 Answers9

2

I got a simple answer for that:

Just add setRetainInstance(true); to the Fragment's onAttach(Activity activity) or onActivityCreated(Bundle savedInstanceState). These two are call-backs in the Fragment Class.

So basically, what setRetainInstance(true) does is: It maintains the state of your fragment as it is, when it goes through:

  • onPause();
  • onStop();

It maintains the instance of the Fragment no matter what the Activity goes through. The problem with it could be, if there are too many Fragments, it may put a strain on the System.

Hope it helps.

@Override
public void onAttach(Activity activity) {
super.onAttach(activity);

setRetainInstance(true);

}

Open for Correction as always. Regards, Edward Quixote.

Edward Quixote
  • 350
  • 5
  • 16
1

It seems that, when the screen is rotated and the app restarted, it is recreating each Fragment by calling the default constructor for the Fragment's class.

Martin Ellison
  • 1,043
  • 1
  • 13
  • 25
0

My code was a little different, but I believe our problem is the same.

In the onTabSelected I didn't use replace, I use add when is the first time creating the fragment and attach if isn't. In the onTabUnselected I use detach.

The problem is that when the view is destroyed, my Fragment was attached to the FragmentManager and never destroyed. To solve that I implemented on the onSaveInstanceBundle to detach the fragment from the FragmentManager.

The code was something like that:

FragmentTransition ft = getSupportFragmentManager().begin();
ft.detach(myFragment);
ft.commit();

In the first try I put that code in the onDestroy, but I get a exception telling me that I couldn't do it after the onSaveInstanceBundle, so I moved the code to the onSaveInstanceBundle and everything worked.

Sorry but the place where I work don't allow me to put the code here on StackOverflow. This is what I remember from the code. Feel free to edit the answer to add the code.

Ferrmolina
  • 2,737
  • 2
  • 30
  • 46
jonathanrz
  • 4,206
  • 6
  • 35
  • 58
0

I have encountered the same issue and used the following workaround:

in the fragment's onCreateView begining of:

if (mView != null) {
    // Log.w(TAG, "Fragment initialized again");
    ((ViewGroup) mView.getParent()).removeView(mView);
    return mView;
}
// normal onCreateView
mView = inflater.inflate(R.layout...)
Iftah
  • 9,512
  • 2
  • 33
  • 45
0

I think this is a fool proof way to avoid re-inflating of the root view of the fragment:

private WeakReference<View> mRootView;
private LayoutInflater mInflater;

/**
 * inflate the fragment layout , or use a previous one if already stored <br/>
 * WARNING: do not use in any function other than onCreateView
 * */
private View inflateRootView() {
    View rootView = mRootView == null ? null : mRootView.get();
    if (rootView != null) {
        final ViewParent parent = rootView.getParent();
        if (parent != null && parent instanceof ViewGroup)
            ((ViewGroup) parent).removeView(rootView);
        return rootView;
    }
    rootView = mFadingHelper.createView(mInflater);
    mRootView = new WeakReference<View>(rootView);
    return rootView;
}


@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
    mInflater=inflater!=null?inflater:LayoutInflater.from(getActivity());
    final View view = inflateRootView();
    ... //update your data on the views if needed
}
android developer
  • 114,585
  • 152
  • 739
  • 1,270
0

I think you are facing what I faced. I had a thread downloader for json which starts in onCreate() , each time I changed the orientation the thread is called and download is fired. I fixed this using onSaveInstance() and onRestoreInstance() to pass the json response in a list, in combination of checking if the list is not empty, so the extra download is not needed.

I hope this gives you a hint.

Ferrmolina
  • 2,737
  • 2
  • 30
  • 46
Antoine Baqain
  • 392
  • 1
  • 3
0

add android:configChanges="orientation|screenSize" in the manifest file

zacharia
  • 1,083
  • 2
  • 10
  • 22
0

To protect activity recreate try to add configChanges in your Activity tag (in manifest), like:

android:configChanges="keyboardHidden|orientation|screenSize"
Sufian
  • 6,405
  • 16
  • 66
  • 120
rasfarrf5
  • 219
  • 1
  • 5
0

I solved this problem by using below code.

private void loadFragment(){
     LogUtil.l(TAG,"loadFragment",true);
     fm = getSupportFragmentManager();
     Fragment hf = fm.findFragmentByTag("HOME");
     Fragment sf = fm.findFragmentByTag("SETTING");
     if(hf==null) {
         homeFragment = getHomeFragment();// new HomeFragment();
         settingsFragment = getSettingsFragment();// new Fragment();
         fm.beginTransaction().add(R.id.fm_place, settingsFragment, "SETTING").hide(settingsFragment).commit();
         fm.beginTransaction().add(R.id.fm_place, homeFragment, "HOME").commit();
         activeFragment = homeFragment;
     }else{
         homeFragment = hf;
         settingsFragment = sf;
         activeFragment = sf;
     }
 }

Initiate this method in OnCreate();

Sharn25
  • 1
  • 1