Фрагмент onCreateView и onActivityCreated вызывается дважды

101

Я разрабатываю приложение с использованием Android 4.0 ICS и фрагментов.

Рассмотрим этот модифицированный пример из демонстрационного приложения API ICS 4.0.3 (API level 15):

public class FragmentTabs extends Activity {

private static final String TAG = FragmentTabs.class.getSimpleName();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    final ActionBar bar = getActionBar();
    bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
    bar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);

    bar.addTab(bar.newTab()
            .setText("Simple")
            .setTabListener(new TabListener<SimpleFragment>(
                    this, "mysimple", SimpleFragment.class)));

    if (savedInstanceState != null) {
        bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
        Log.d(TAG, "FragmentTabs.onCreate tab: " + savedInstanceState.getInt("tab"));
        Log.d(TAG, "FragmentTabs.onCreate number: " + savedInstanceState.getInt("number"));
    }

}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
}

public static class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private final Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;
    private final Bundle mArgs;
    private Fragment mFragment;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        this(activity, tag, clz, null);
    }

    public TabListener(Activity activity, String tag, Class<T> clz, Bundle args) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mArgs = args;

        // Check to see if we already have a fragment for this tab, probably
        // from a previously saved state.  If so, deactivate it, because our
        // initial state is that a tab isn't shown.
        mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
        if (mFragment != null && !mFragment.isDetached()) {
            Log.d(TAG, "constructor: detaching fragment " + mTag);
            FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
            ft.detach(mFragment);
            ft.commit();
        }
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
            Log.d(TAG, "onTabSelected adding fragment " + mTag);
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            Log.d(TAG, "onTabSelected attaching fragment " + mTag);
            ft.attach(mFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            Log.d(TAG, "onTabUnselected detaching fragment " + mTag);
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        Toast.makeText(mActivity, "Reselected!", Toast.LENGTH_SHORT).show();
    }
}

public static class SimpleFragment extends Fragment {
    TextView textView;
    int mNum;

    /**
     * When creating, retrieve this instance's number from its arguments.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(FragmentTabs.TAG, "onCreate " + (savedInstanceState != null ? ("state " + savedInstanceState.getInt("number")) : "no state"));
        if(savedInstanceState != null) {
            mNum = savedInstanceState.getInt("number");
        } else {
            mNum = 25;
        }
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.d(TAG, "onActivityCreated");
        if(savedInstanceState != null) {
            Log.d(TAG, "saved variable number: " + savedInstanceState.getInt("number"));
        }
        super.onActivityCreated(savedInstanceState);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        Log.d(TAG, "onSaveInstanceState saving: " + mNum);
        outState.putInt("number", mNum);
        super.onSaveInstanceState(outState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        Log.d(FragmentTabs.TAG, "onCreateView " + (savedInstanceState != null ? ("state: " + savedInstanceState.getInt("number")) : "no state"));
        textView = new TextView(getActivity());
        textView.setText("Hello world: " + mNum);
        textView.setBackgroundDrawable(getResources().getDrawable(android.R.drawable.gallery_thumb));
        return textView;
    }
}

}

Вот результат, полученный при запуске этого примера и последующем повороте телефона:

06-11 11:31:42.559: D/FragmentTabs(10726): onTabSelected adding fragment mysimple
06-11 11:31:42.559: D/FragmentTabs(10726): onCreate no state
06-11 11:31:42.559: D/FragmentTabs(10726): onCreateView no state
06-11 11:31:42.567: D/FragmentTabs(10726): onActivityCreated
06-11 11:31:45.286: D/FragmentTabs(10726): onSaveInstanceState saving: 25
06-11 11:31:45.325: D/FragmentTabs(10726): onCreate state 25
06-11 11:31:45.340: D/FragmentTabs(10726): constructor: detaching fragment mysimple
06-11 11:31:45.340: D/FragmentTabs(10726): onTabSelected attaching fragment mysimple
06-11 11:31:45.348: D/FragmentTabs(10726): FragmentTabs.onCreate tab: 0
06-11 11:31:45.348: D/FragmentTabs(10726): FragmentTabs.onCreate number: 0
06-11 11:31:45.348: D/FragmentTabs(10726): onCreateView state: 25
06-11 11:31:45.348: D/FragmentTabs(10726): onActivityCreated
06-11 11:31:45.348: D/FragmentTabs(10726): saved variable number: 25
06-11 11:31:45.348: D/FragmentTabs(10726): onCreateView no state
06-11 11:31:45.348: D/FragmentTabs(10726): onActivityCreated

Мой вопрос: почему onCreateView и onActivityCreated вызываются дважды? В первый раз с Bundle с сохраненным состоянием, а во второй раз с нулевым значением savedInstanceState?

Это вызывает проблемы с сохранением состояния фрагмента при вращении.

Дэйв
источник
2
Думаю, этот вопрос может быть связан с stackoverflow.com/a/8678705/404395
marioosh

Ответы:

45

Я тоже какое-то время ломал голову над этим, и, поскольку объяснение Дэйва немного сложно понять, я опубликую свой (очевидно рабочий) код:

private class TabListener<T extends Fragment> implements ActionBar.TabListener {
    private Fragment mFragment;
    private Activity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public TabListener(Activity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
        mFragment=mActivity.getFragmentManager().findFragmentByTag(mTag);
    }

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        if (mFragment == null) {
            mFragment = Fragment.instantiate(mActivity, mClass.getName());
            ft.replace(android.R.id.content, mFragment, mTag);
        } else {
            if (mFragment.isDetached()) {
                ft.attach(mFragment);
            }
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        if (mFragment != null) {
            ft.detach(mFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
    }
}

Как видите, он очень похож на образец Android, за исключением того, что он не отсоединяется в конструкторе и использует замену вместо добавления .

После долгих размышлений и проб и ошибок я обнаружил, что обнаружение фрагмента в конструкторе, похоже, волшебным образом устраняет двойную проблему onCreateView (я предполагаю, что она просто заканчивается нулевым значением для onTabSelected при вызове через путь ActionBar.setSelectedNavigationItem (), когда сохранение / восстановление состояния).

Staffan
источник
Прекрасно работает! Ты спас мне сон! Спасибо :)
jaibatrik
вы также можете использовать fragment.getClass (). getName (), если вы хотите удалить переменную класса и удалить параметр из вызова
Бен Сьюардс
Прекрасно работает с образцом Android "предыдущая ссылка TabListener" - tnx. Новейший "образец ссылки TabListener" Android [по состоянию на 4 ix 2013] действительно, очень неверен.
Grzegorz Dev
где вызов метода ft.commit () ??
MSaudi
1
@MuhammadBabar см stackoverflow.com/questions/23248789/... . Если использовать addвместо replaceи повернуть экран, у вас будет много фрагментов » onCreateView().
CoolMind
26

Хорошо, вот что я узнал.

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

В конструкторе TabListener происходило то, что вкладка отключалась, если она была найдена и прикреплена к действию. Увидеть ниже:

mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
    if (mFragment != null && !mFragment.isDetached()) {
        Log.d(TAG, "constructor: detaching fragment " + mTag);
        FragmentTransaction ft = mActivity.getFragmentManager().beginTransaction();
        ft.detach(mFragment);
        ft.commit();
    }

Позже в действии onCreate ранее выбранная вкладка была выбрана из состояния сохраненного экземпляра. Увидеть ниже:

if (savedInstanceState != null) {
    bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
    Log.d(TAG, "FragmentTabs.onCreate tab: " + savedInstanceState.getInt("tab"));
    Log.d(TAG, "FragmentTabs.onCreate number: " + savedInstanceState.getInt("number"));
}

Когда вкладка была выбрана, она будет повторно подключена в обратном вызове onTabSelected.

public void onTabSelected(Tab tab, FragmentTransaction ft) {
    if (mFragment == null) {
        mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
        Log.d(TAG, "onTabSelected adding fragment " + mTag);
        ft.add(android.R.id.content, mFragment, mTag);
    } else {
        Log.d(TAG, "onTabSelected attaching fragment " + mTag);
        ft.attach(mFragment);
    }
}

Присоединяемый фрагмент - это второй вызов методов onCreateView и onActivityCreated. (Первый - когда система воссоздает активность и все прикрепленные фрагменты). В первый раз пакет onSavedInstanceState сохранил бы данные, но не во второй раз.

Решение состоит в том, чтобы не отсоединять фрагмент в конструкторе TabListener, просто оставьте его прикрепленным. (Вам все равно нужно найти его в FragmentManager по его тегу) Кроме того, в методе onTabSelected я проверяю, не отсоединен ли фрагмент, прежде чем прикрепить его. Что-то вроде этого:

public void onTabSelected(Tab tab, FragmentTransaction ft) {
            if (mFragment == null) {
                mFragment = Fragment.instantiate(mActivity, mClass.getName(), mArgs);
                Log.d(TAG, "onTabSelected adding fragment " + mTag);
                ft.add(android.R.id.content, mFragment, mTag);
            } else {

                if(mFragment.isDetached()) {
                    Log.d(TAG, "onTabSelected attaching fragment " + mTag);
                    ft.attach(mFragment);
                } else {
                    Log.d(TAG, "onTabSelected fragment already attached " + mTag);
                }
            }
        }
Дэйв
источник
4
Упомянутые решения «не отсоединять фрагмент в конструкторе TabListener» приводят к тому, что фрагменты вкладки перекрывают друг друга. Я вижу содержимое остальных фрагментов. У меня это не работает.
Аксель Фатих
@ flock.dux Я не совсем понимаю, что вы имеете в виду, говоря о наложении друг на друга. Android позаботится о том, как они будут размещены, поэтому мы просто указываем прикрепление или отсоединение. Должно быть что-то еще. Может быть, если вы зададите новый вопрос с примером кода, мы сможем понять, что для вас происходит.
Дэйв
1
У меня была такая же проблема (несколько вызовов конструктора фрагментов из Android). Ваш вывод решает мою проблему: я не понял, что все фрагменты, которые присоединяются к действию, когда происходит изменение конфигурации (вращение телефона), воссоздаются и добавляются обратно к действию. (что имеет смысл)
Евгений
26

У меня была такая же проблема с простым Activity, содержащим только один фрагмент (который иногда заменялся). Затем я понял, что использую onSaveInstanceState только во фрагменте (и onCreateView для проверки сохраненногоInstanceState), а не в действии.

При включении устройства действие, содержащее фрагменты, перезапускается и вызывается onCreated. Туда я прикрепил нужный фрагмент (что правильно при первом запуске).

На повороте устройства Android сначала воссоздал видимый фрагмент, а затем вызвал onCreate действия, в котором был прикреплен мой фрагмент, заменив исходный видимый фрагмент.

Чтобы этого избежать, я просто изменил свою активность, чтобы проверить saveInstanceState:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

if (savedInstanceState != null) {
/**making sure you are not attaching the fragments again as they have 
 been 
 *already added
 **/
 return; 
 }
 else{
  // following code to attach fragment initially
 }

 }

Я даже не перезаписывал onSaveInstanceState активности.

Гуннар Бернштейн
источник
Спасибо. Это помогло мне с AppCompatActivity + PreferenceFragmentCompat и сбой при отображении диалогов во фрагменте предпочтений после изменения ориентации, поскольку диспетчер фрагментов был пустым при создании второго фрагмента.
РК
12

В двух ответах, за которые проголосовали, показаны решения для Activity с режимом навигации NAVIGATION_MODE_TABS, но у меня была такая же проблема с файлом NAVIGATION_MODE_LIST. Из-за этого мои фрагменты необъяснимо теряли свое состояние при изменении ориентации экрана, что очень раздражало. К счастью, благодаря их полезному коду мне удалось в этом разобраться.

В основном, при использовании навигации по списку, onNavigationItemSelected () is automatically called when your activity is created/re-created, whether you like it or not. To prevent your Fragment'sonCreateView () from being called twice, this initial automatic call toonNavigationItemSelected () should check whether the Fragment is already in existence inside your Activity. If it is, return immediately, because there is nothing to do; if it isn't, then simply construct the Fragment and add it to the Activity like you normally would. Performing this check prevents your Fragment from needlessly being created again, which is what causesonCreateView () будет вызываться дважды!

Смотрите мою onNavigationItemSelected()реализацию ниже.

public class MyActivity extends FragmentActivity implements ActionBar.OnNavigationListener
{
    private static final String STATE_SELECTED_NAVIGATION_ITEM = "selected_navigation_item";

    private boolean mIsUserInitiatedNavItemSelection;

    // ... constructor code, etc.

    @Override
    public void onRestoreInstanceState(Bundle savedInstanceState)
    {
        super.onRestoreInstanceState(savedInstanceState);

        if (savedInstanceState.containsKey(STATE_SELECTED_NAVIGATION_ITEM))
        {
            getActionBar().setSelectedNavigationItem(savedInstanceState.getInt(STATE_SELECTED_NAVIGATION_ITEM));
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState)
    {
        outState.putInt(STATE_SELECTED_NAVIGATION_ITEM, getActionBar().getSelectedNavigationIndex());

        super.onSaveInstanceState(outState);
    }

    @Override
    public boolean onNavigationItemSelected(int position, long id)
    {    
        Fragment fragment;
        switch (position)
        {
            // ... choose and construct fragment here
        }

        // is this the automatic (non-user initiated) call to onNavigationItemSelected()
        // that occurs when the activity is created/re-created?
        if (!mIsUserInitiatedNavItemSelection)
        {
            // all subsequent calls to onNavigationItemSelected() won't be automatic
            mIsUserInitiatedNavItemSelection = true;

            // has the same fragment already replaced the container and assumed its id?
            Fragment existingFragment = getSupportFragmentManager().findFragmentById(R.id.container);
            if (existingFragment != null && existingFragment.getClass().equals(fragment.getClass()))
            {
                return true; //nothing to do, because the fragment is already there 
            }
        }

        getSupportFragmentManager().beginTransaction().replace(R.id.container, fragment).commit();
        return true;
    }
}

Я позаимствовал вдохновение для этого решения от здесь .

XåpplI'-I0llwlg'I -
источник
Это решение работает для моей аналогичной проблемы с навигационным ящиком. Я нахожу существующий фрагмент по идентификатору и проверяю, имеет ли он тот же класс, что и новый фрагмент, прежде чем воссоздать его.
Уильям
8

Мне кажется, это из-за того, что вы каждый раз создаете экземпляр своего TabListener ... поэтому система воссоздает ваш фрагмент из сохраненногоInstanceState, а затем вы делаете это снова в своем onCreate.

Вы должны обернуть это в a, if(savedInstanceState == null)чтобы он срабатывал только в том случае, если нет saveInstanceState.

Барак
источник
Я не думаю, что это правильно. Когда я помещаю свой код addTab в блок if, фрагмент прикрепляется к активности, но вкладок нет. Похоже, вам нужно каждый раз добавлять вкладки в методе onCreate. Я продолжу изучать это и опубликую больше, если я лучше понимаю.
Dave