Фрагмент onCreateView і onActivityCreate викликається двічі


101

Я розробляю додаток за допомогою Android 4.0 ICS та фрагментів.

Розглянемо цей модифікований приклад із демонстраційного прикладу програми API ICS 4.0.3 (рівень API 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 і onActivityCreate двічі викликаються? Перший раз із розшаруванням із збереженим станом і вдруге з нульовим збереженимInstanceState?

Це викликає проблеми із збереженням стану фрагмента при обертанні.


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 (), коли збереження / відновлення стану).


Працює чудово! Ти врятував мій нічний сон! Дякую :)
jaibatrik

ви також можете використовувати fragment.getClass (). getName (), якщо ви хочете видалити змінну класу та видалити параметр з виклику
Ben Sewards

Відмінно працює з зразком Android "попередній номер TabListener" - tnx. Найновіший Android "зразок TabListener. Зразок" [як 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 і onActivityCreate. (Перше, коли система відтворює активність та всі додані фрагменти) Перший раз, коли пакет 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). Виявлення вирішує мою проблему: те, що я не зрозумів, - це те, що всі фрагменти, пов’язані з діяльністю, коли відбувається зміна конфігурації (телефон обертається), відтворюються та додаються назад до активності. (що має сенс)
eugene

26

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

На пристрої поверніть активність, що містить фрагменти, перезапускається і викликається onCreate. Там я вклав необхідний фрагмент (що правильно на першому початку).

На пристрої включення Android спочатку створив фрагмент, який було видно, а потім викликав onCreate з вмісту, що містить вміст, де був доданий мій фрагмент, замінивши таким чином оригінальний видимий.

Щоб уникнути цього, я просто змінив свою діяльність, щоб перевірити, чи збережений файлInstanceState:

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 і збій під час показу діалогів у фрагменті уподобань після зміни орієнтації, оскільки менеджер фрагментів був нульовим при створенні другого фрагмента.
RoK

12

У двох оприлюднених відповідях тут показано рішення для діяльності з режимом навігації NAVIGATION_MODE_TABS, але у мене була та сама проблема з a 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;
    }
}

Натхнення для цього рішення я запозичив звідси .


Це рішення працює для моєї аналогічної проблеми з навігаційним ящиком. Я знаходжу існуючий фрагмент за ідентифікатором і перевіряю, чи він має той самий клас, як новий фрагмент, перш ніж його відтворити.
Вільям

8

Мені здається, що це відбувається тому, що ви інстанціюєте свій TabListener кожен раз ... тому система відтворює ваш фрагмент із збереженогоInstanceState, а потім ви робите це знову в своєму onCreate.

Ви повинні обернути це if(savedInstanceState == null)так, що він спрацьовує лише якщо немає збереженогоInstanceState.


Я не думаю, що це правильно. Коли я загортаю свій код addTab у блок if, фрагмент додається до активності, але вкладок немає. Здається, вам потрібно щоразу додавати вкладки методом onCreate. Я буду продовжувати вивчати це та публікувати більше, коли я краще розумію.
Дейв
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.