Хост на Android FragmentTab и фрагменти във фрагменти

Имам приложение с йерархия като тази:

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

Всички изгледи на фрагменти се увеличават от ресурси.

Когато приложението стартира, всичко се появява и изглежда добре. Когато превключвам от първия раздел към друг раздел и обратно, получавам раздуващи се изключения, опитвайки се да пресъздам изгледите на раздел 1.

Ако копаем малко по-дълбоко, ето какво се случва:

  • При първото зареждане, раздуването на разделителния изглед води до добавяне на неговите два дъщерни фрагмента към мениджъра на фрагменти.
  • При превключване от първия раздел, неговият изглед се унищожава, но неговите дъщерни фрагменти остават в диспечера на фрагменти
  • При превключване обратно към първия раздел, изгледът се увеличава отново и тъй като старите дъщерни фрагменти все още са в диспечера на фрагменти, се хвърля изключение, когато новите дъщерни фрагменти се инстанциират (чрез инфлация)

Заобиколих това, като премахнах дъщерните фрагменти от мениджъра на фрагменти (използвам Mono) и сега мога да превключвам раздели без изключение.

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();
}

Така че имам няколко въпроса:

  1. Дали горното е правилният начин да направите това?
  2. Ако не, как трябва да го направя?
  3. Така или иначе, как запазването на състоянието на екземпляра се свързва с всичко това, така че да не загубя състояние на изглед при превключване на раздели?

person Brad Robinson    schedule 11.03.2013    source източник


Отговори (2)


Не съм сигурен как да направя това в Mono, но за да добавите дъщерни фрагменти към друг фрагмент, не можете да използвате FragmentManager на Activity. Вместо това трябва да използвате ChildFragmentManager на хостинга Fragment:

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

Основният FragmentManager от Activity управлява вашите раздели.
ChildFragmentManager от tab1 управлява разделените изгледи.

person Streets Of Boston    schedule 11.03.2013
comment
Добре, това звучи обещаващо. Как да кажа на надувателя да използва мениджъра на дъщерни фрагменти вместо родителския? - person Brad Robinson; 12.03.2013
comment
Добавяте дъщерния фрагмент във вашия родителски фрагмент, като накарате кода на родителския фрагмент да използва ChildFragmentManager, за да добави своите деца. След това надуващото средство, което вашият метод за обратно извикване onCreateView на вашия (дъщен) фрагмент предоставя: developer.android.com/reference/android/app/, android.view.ViewGroup, android.os.Bundle). - person Streets Of Boston; 12.03.2013
comment
Вероятно трябва да направите още малко гугъл/стек, за да видите как да се справите с дъщерните фрагменти в Mono. Не мога да ви помогна там (използвам Java). - person Streets Of Boston; 12.03.2013
comment
Mono е почти абсолютно същото... това е същото api, различен език. Това, което не разбирам, е този бит: като кодът на родителския фрагмент използва ChildFragmentManager, за да добави своите деца. Дъщерните фрагменти са дефинирани в axml на изгледа на родителския фрагмент, така че техният инстанциран и добавен към мениджъра на фрагменти, тъй като родителският изглед е раздут. И така, как да кажа на инфлатора да използва диспечера на дъщерните фрагменти? Трябва ли да забравите надуващото устройство и да създадете ръчно дъщерните фрагменти? - person Brad Robinson; 13.03.2013

Добре, най-накрая разбрах това:

Както беше предложено по-горе, първо промених създаването на фрагменти да се извършва програмно и ги добавих към мениджъра на дъщерни фрагменти, така:

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;
}

Както се очакваше, всеки път, когато превключвам раздели, ще бъде създаден допълнителен екземпляр на Lhs/RhsFragment, но забелязах, че OnCreateView на стария Lhs/RhsFragment също ще бъде извикан. Така че след всяко превключване на раздели ще има още едно извикване на OnCreateView. Превключване на раздели 10 пъти = 11 извиквания на OnCreateView. Това очевидно е грешно.

Разглеждайки изходния код за FragmentTabHost, виждам, че той просто отделя и прикачва отново фрагмента от съдържанието на раздела при превключване на раздели. Изглежда, че ChildFragmentManager на родителския фрагмент поддържа дъщерните фрагменти наоколо и автоматично пресъздава техните изгледи, когато родителският фрагмент се прикачи отново.

И така, преместих създаването на фрагменти в OnCreate и само ако не зареждаме от запазено състояние:

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);
}

Това поправи създаването на допълнителните изгледи и разделите за превключване вече основно работят.

Следващият въпрос беше запазването и възстановяването на състоянието на изгледа. В дъщерните фрагменти трябва да запазя и възстановя текущо избрания елемент. Първоначално имах нещо подобно (това е 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;
}

Проблемът с това е, че хостът на раздела не извиква OnSaveInstanceState при превключване на раздели. По-скоро дъщерният фрагмент се поддържа жив и неговата променлива _selection може просто да бъде оставена сама.

Така че преместих кода за управление на селекцията в 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);
}

Сега всичко изглежда работи перфектно, както при превключване на раздели, така и при промяна на ориентация.

person Brad Robinson    schedule 14.03.2013