Хост 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 вашего (дочернего) фрагмента: ="nofollow noreferrer">developer.android.com/reference/android/app/, android.view.ViewGroup, android.os.Bundle). - person Streets Of Boston; 12.03.2013
comment
Вероятно, вам нужно еще немного поискать в Google/stackoverlowing, чтобы увидеть, как работать с дочерними фрагментами в Mono. Я не могу вам помочь (я использую Java). - person Streets Of Boston; 12.03.2013
comment
Моно почти то же самое... это тот же 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