質問

I have been playing around with FragmentTabHost from Android v4 Support library for a while and came upon a serious issue that I couldn't resolve. My target requirements were as follows.

1) Fragment that would include tabs built also from Fragments which include nested Fragments as well, with hierarchy like this:

  • android.support.v4.app.Fragment (HostFragment)
    • android.support.v4.app.FragmentTabHost (TabHost)
      • android.support.v4.app.Fragment (Tab1Fragment)
        • android.support.v4.app.Fragment (Tab1Fragment1)
        • android.support.v4.app.Fragment (Tab1Fragment2)
        • android.support.v4.app.Fragment (Tab1Fragment3)
      • android.support.v4.app.Fragment (Tab2Fragment)
        • android.support.v4.app.Fragment (Tab2Fragment1)
        • android.support.v4.app.Fragment (Tab2Fragment2)

HostFragment + TabHost setup was done according to what says in the documentation here.

2) Upon screen rotation retain status of HostFragment as recreation of this setup is a resources consuming operation and besides layout order nothing changes on this screen so no extra work would be needed to just display the screen. Just return a different layout in onCreateView callback of Tab1 and Tab2 and reattach existing fragments to the same id's.

To accomplish that one would think that just setting HostFragment.setRetainInstance(true) would do the work and, partially, it does. Nothing was getting recreated, tabs were preserved as they should, all worked fine. Now let's get to the point.

The Problem

The big problem came up a bit later. It appears that Tab1Fragment1 (and all other TabXFragmentY for that matter) are not attached to new Activity that is created upon rotation. They are only attached on the first run of the Activity and then nothing is being done when user rotates the screen.

The Consequences

This causes two big issues for me (might be there are more):

  1. Initial Activity is leaked because all the Fragments in the TabHost are attached to it for no reason at all.
  2. When you call getActivity() on any of the TabXFragmentY you will get the old Activity and the dreadful "cannot perform operation after onSavedInstance" exception.

This leads to serious issues when you want something from the parent Activity. Also, it doesn't happen when setting HostFragment.setRetainInstance(false) everything get's recreated and works fine.

The Question

Is it an issue that I am only seeing? I couldn't find anything on the topic and I have reviewed my code a hundred times.

I have also tried changing com.example.android.supportv4.app.FragmentTabsFragmentSupport from the v4 examples, I did set it to retain it's instance and added a logging information in one of its nested fragments on every onAttach call to verify and I can see there still the same issue. I am puzzled. Help.

役に立ちましたか?

解決

Recently I've been working on a TabHost with Fragments and found the same issue. Basically, you need to control which Fragments are being attached/detached, in my case, I do this within the onTabChanged() event.

I have a TabInfo class where I store the following information for each Tab

  • The name of the Fragment (for identification purposes).
  • The Fragment to attach.
  • The TabHost.TabSpec specification, as if you want to remove some tab, there's the info for recreating the rest of them as TabHost is a bit tricky for removing tabs.
  • The Bundle associated (basically, the saved instance prior to a configuration change).

Also, I need to keep track of the lastTab and the newTab opened as TabHost doesn't have a native way of knowing which TabHost have been just closed, so I do it declaring a variable class-wide. And this is how I handle it right now, I'll try to add as many comments as I can:

@Override
public void onTabChanged(final String tag) {
  // I get the info for the Tab just triggered using its tag
  final TabInfo newTab = mTabInfo.get(tag);

  // If there's actually been a tab change...
  if (lastTab != newTab) {
    // You'll have to make a transaction for replacing the Fragment
    final FragmentTransaction ft = this.getSupportFragmentManager().beginTransaction();

    // If the last tab actually has a Fragment associated to it
    if ((lastTab != null) && (lastTab.getFragment() != null)) {
      // In my case I've an additional level of complexion, as I have a nested Fragment
      // inside my content Fragment. So I have to remove it first prior to detaching
      // the parent Fragment. This is not needed if you have just one Fragment as content.
      final Fragment loginFrag = (Fragment) lastTab.getFragment().getActivity().getSupportFragmentManager().findFragmentById(lastTab.getLoginFragId());
      ft.remove(loginFrag);

      // And this is what does the trick: I initially was calling detach() instead of remove()
      // but seems that with some versions there's a problem that makes not apply it,
      // calling remove will actually remove this Fragment
      ft.remove(lastTab.getFragment());
    }

    // You've detached the old Fragment, you have now to attach the new one
    if (newTab != null) {
      if (newTab.getFragment() == null) {
        // Inflate the new content if it's the first time the tab has been fired
        final TabFragmentInflater tabInf = new TabFragmentInflater();
        newTab.setFragment(Fragment.instantiate(this, tabInf.getClass().getName(), newTab.getArgs()));
        ft.add(R.id.realtabcontent, newTab.getFragment(), newTab.getTag());
      }
      else {
        // if not, just attach its fragment
        ft.attach(newTab.getFragment());
      }
    }

    ft.commit();
    this.getSupportFragmentManager().executePendingTransactions();

    lastTab = newTab;
  }
}

---- EDIT ----

Answering to your questions:

  • I indeed call newTabSpec() over the TabHost, it's in a separate method I didn't include because I just included the onTabChanged() callback. You have to create the tabs normally, this onTabChanged() method just gets fired when you click on tabs. For the creation, I do something like this:

    private void initTabHost(final Bundle args) {
      final TabHost th = (TabHost) findViewById(android.R.id.tabhost);
      th.setup();
    
      // I have a HashMap called fragMap where as the key I define the tab's name
      // And as the value, I have an Integer which is a unique identifier
      // to know what to inflate when I call the TabFragmentInflater (I will
      // add the code below). You can perfectly add it as an id or a tag also.
      for (final String tablabel : fragMap.keySet()) {
        final TabHost.TabSpec tabSpec = th.get().newTabSpec(tabname).setIndicator(tabname);
        // Here I initialize a TabInfo object for this tab, which will include additional 
        // Handling info: Name of tab, Tab Spec, The unique ID I explained above,
        // the forth argument is irrelevant in your example, and args (saved instance)
        final TabInfo tabInfo = new TabInfo(tabname, tabSpec, fragMap.get(tablabel), R.id.someLayout, args);
    
        // This is not actually the TabHost's `addTab()` method, I'll call it inside
        // this method (see below)
        MyClass.addTab(this, th, tabSpec, tabInfo);
    
        // I have to be able to keep tracking of that info
        mTabInfo.put(tabInfo.getTag(), tabInfo);
      }
    
      // For the tab creation, I force it to start on the first tab
      this.onTabChanged(firstTabNameTag);
      th.setOnTabChangedListener(this);
    }
    

The addTab() method is very simple, it inflates the object and detaches it if needed.

private static void addTab(final MyClass activity, final TabHost tabHost, final TabHost.TabSpec tabSpec, final TabInfo tabInfo) {
  tabSpec.setContent(new TabFactory(activity));    // This is just an empty View
  final String tag = tabSpec.getTag();

  // Here I check if there's already a Fragment for that tab, probably in a 
  // previously saved state. If this happens, we deactivate it, because our
  // former state for that tab is "not shown".
  tabInfo.setFragment(activity.getSupportFragmentManager().findFragmentByTag(tag));
  if ((tabInfo.getFragment() != null) && (!tabInfo.getFragment().isDetached())) {
    final FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction();
    ft.detach(tabInfo.getFragment());
    ft.commit();

    activity.getSupportFragmentManager().executePendingTransactions();
  }

  // Actually, there's where I call the "official" `addTab()` from `TabHost`
  tabHost.addTab(tabSpec);
}
  • So there's just the TabFragmentInflater left. It's just a Fragment that inflates the according layout depending on the unique ID I mentioned above, so it's something like this:

    @Override
    public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
      fragId = getArguments().getInt("fragid");
    
      if (view != null) {
        ViewGroup parent = (ViewGroup) view.getParent();
        if (parent != null)
          parent.removeView(view);
      }
    
      try {
        switch (fragId) {
          case 1:         // My first tab...
            final LinearLayout fragLayout = (LinearLayout) inflater.inflate(R.layout.myfirsttab_fragment_layout, container, false);
    
            ...      
            return fragLayout;
    
          case 2:           // My second tab
            fragLayout = (LinearLayout) inflater.inflate(R.layout.mysecondtab_fragment_layout, container, false);
    
            ...
            return fragLayout;
    
          ...
        }
      } 
      catch (final InflateException e) { return view; }
    
      return null;    
    }
    
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top