Android FragmentTab host and Fragments inside Fragments
Asked Answered
C

2

6

I have an app with hierarchy like this:

FragmentTabHost (Main Activity)
  - Fragment (tab 1 content - splitter view)
    - Fragment (lhs, list)
    - Framment (rhs, content view)
  - Fragment (tab 2 content)
  - Fragment (tab 2 content)

All fragment views are being inflated from resources.

When the app starts everything appears and looks fine. When I switch from the first tab to another tab and back again I get inflate exceptions trying to recreate tab 1's views.

Digging a little deeper, this is what's happening:

  • On the first load, inflating the splitter view causes its two child fragments to be added to the fragment manager.
  • On switching away from the first tab, it's view is destroyed but it's child fragments are left in the fragment manager
  • On switching back to the first tab, the view is re-inflated and since the old child fragments are still in the fragment manager an exception is thrown when the new child fragments are instantiated (by inflation)

I've worked around this by removing the child fragments from the fragment manager (I'm using Mono) and now I can switch tabs without the exception.

public override void OnDestroyView()
{
    var ft = FragmentManager.BeginTransaction();
    ft.Remove(FragmentManager.FindFragmentById(Resource.Id.ListFragment));
    ft.Remove(FragmentManager.FindFragmentById(Resource.Id.ContentFragment));
    ft.Commit();

    base.OnDestroyView();
}

So I have a few questions:

  1. Is the above the correct way to do this?
  2. If not, how should I be doing it?
  3. Either way, how does saving instance state tie into all of this so that I don't lose view state when switching tabs?
Cheerio answered 11/3, 2013 at 22:22 Comment(0)
R
3

I'm not sure how to do this in Mono, but to add child fragments to another fragment, you can't use the FragmentManager of the Activity. Instead, you have to use the ChildFragmentManager of the hosting Fragment:

http://developer.android.com/reference/android/app/Fragment.html#getChildFragmentManager() http://developer.android.com/reference/android/support/v4/app/Fragment.html#getChildFragmentManager()

The main FragmentManager of the Activity handles your tabs.
The ChildFragmentManager of tab1 handles the split views.

Reimport answered 11/3, 2013 at 23:19 Comment(4)
OK, that sounds promising. How do I tell the inflater to use the child fragment manager instead of the parent one?Cheerio
You add the child fragment inside your parent fragment by having the parent fragment's code use the ChildFragmentManager to add its children. Then the inflater that your onCreateView callback method of your (child) fragment provides: developer.android.com/reference/android/app/…, android.view.ViewGroup, android.os.Bundle).Reimport
You probably need to do some more googling/stackoverlowing to see how to deal with child-fragments in Mono. I can't help you there (i use Java).Reimport
Mono is pretty much exactly the same... it's the same api, different language. What I don't get is this bit: "by having the parent fragment's code use the ChildFragmentManager to add its children". The child fragments are defined in the axml of parent fragment's view, so their instantiated and added to the fragment manager as the parent view is inflated. So, how do I tell the inflater to use the child fragment manager? Should forget the inflater and manually create the child fragments?Cheerio
C
2

OK, I finally figured this out:

As suggested above, first I changed the fragment creation to be done programatically and had them added to the child fragment manager, like so:

public override View OnCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstance)
{
    var view = inflater.Inflate(Resource.Layout.MyView, viewGroup, false);

    // Add fragments to the child fragment manager
    // DONT DO THIS, SEE BELOW 
    var tx = ChildFragmentManager.BeginTransaction();
    tx.Add(Resource.Id.lhs_fragment_frame, new LhsFragment());
    tx.Add(Resource.Id.rhs_fragment_frame, new RhsFragment());
    tx.Commit();

    return view;
}

As expected, each time I switch tabs, an extra instance of Lhs/RhsFragment would be created, but I noticed that the old Lhs/RhsFragment's OnCreateView would also get called. So after each tab switch, there would be one more call to OnCreateView. Switch tabs 10 times = 11 calls to OnCreateView. This is obviously wrong.

Looking at the source code for FragmentTabHost, I can see that it simply detaches and re-attaches the tab's content fragment when switching tabs. It seems the parent Fragment's ChildFragmentManager is keeping the child fragments around and automatically recreating their views when the parent fragment is re-attached.

So, I moved the creation of fragments to OnCreate, and only if we're not loading from saved state:

public override void OnCreate(Bundle savedInstanceState)
{
    base.OnCreate(savedInstanceState);

    if (savedInstanceState == null)
    {
        var tx = ChildFragmentManager.BeginTransaction();
        tx.Add(Resource.Id.lhs_fragment_frame, new LhsFragment());
        tx.Add(Resource.Id.rhs_fragment_frame, new RhsFragment());
        tx.Commit();
    }
}


public override View OnCreateView(LayoutInflater inflater, ViewGroup viewGroup, Bundle savedInstance)
{
    // Don't instatiate child fragments here

    return inflater.Inflate(Resource.Layout.MyView, viewGroup, false);
}

This fixed the creation of the additional views and switching tab's basically worked now.

The next question was saving and restoring view state. In the child fragments I need to save and restore the currently selected item. Originally I had something like this (this is the child fragment's OnCreateView)

public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance)
{
    var view = inflater.Inflate(Resource.Layout.CentresList, container, false);

    // ... other code ommitted ...

    // DONT DO THIS, SEE BELOW 
    if (savedInstance != null)
    {
        // Restore selection
        _selection = savedInstance.GetString(KEY_SELECTION);
    }
    else
    {
        // Select first item
        _selection =_items[0];  
    }

    return view;
}

The problem with this is that the tab host doesn't call OnSaveInstanceState when switching tabs. Rather the child fragment is kept alive and it's _selection variable can be just left alone.

So I moved the code to manage selection to OnCreate:

public override void OnCreate(Bundle savedInstance)
{
    base.OnCreate(savedInstance);

    if (savedInstance != null)
    {
        // Restore Selection
        _selection = savedInstance.GetString(BK_SELECTION);
    }
    else
    {
        // Select first item
        _selection = _items[0];
    }
}

public override View OnCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstance)
{
    // Don't restore/init _selection here

    return inflater.Inflate(Resource.Layout.CentresList, container, false);
}

Now it all seems to be working perfectly, both when switching tabs and changing orientation.

Cheerio answered 14/3, 2013 at 23:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.