Recently I've been working on a TabHost
with Fragment
s and found the same issue. Basically, you need to control which Fragment
s 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 asTabHost
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 theTabHost
, it's in a separate method I didn't include because I just included theonTabChanged()
callback. You have to create the tabs normally, thisonTabChanged()
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; }