Fragment onCreateView i onActivityCreated wywołane dwukrotnie


101

Rozwijam aplikację przy użyciu systemu Android 4.0 ICS i fragmentów.

Rozważmy ten zmodyfikowany przykład z przykładowej aplikacji demonstracyjnej interfejsu API ICS 4.0.3 (poziom 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;
    }
}

}

Oto dane wyjściowe pobrane po uruchomieniu tego przykładu, a następnie obróceniu telefonu:

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

Moje pytanie brzmi: dlaczego onCreateView i onActivityCreated są wywoływane dwukrotnie? Za pierwszym razem z pakietem z zapisanym stanem, a za drugim z zerowym zapisanym stanem instancji?

Powoduje to problemy z zachowaniem stanu fragmentu podczas obrotu.


2
Myślę, że to pytanie może być związane ze stackoverflow.com/a/8678705/404395
marioosh

Odpowiedzi:


45

Przez chwilę też drapałem się w głowę, a ponieważ wyjaśnienie Dave'a jest trochę trudne do zrozumienia, opublikuję mój (najwyraźniej działający) kod:

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

Jak widać, jest to bardzo podobne do przykładu z Androida, poza tym, że nie odłącza się w konstruktorze i używa zamiany zamiast dodawania .

Po wielu próbach i błędach odkryłem, że znalezienie fragmentu w konstruktorze wydaje się sprawiać, że podwójny problem onCreateView w magiczny sposób znika (zakładam, że kończy się on na wartości zerowej dla onTabSelected po wywołaniu przez ścieżkę ActionBar.setSelectedNavigationItem (), gdy stan zapisywania / przywracania).


Działa doskonale! Uratowałeś mi sen! Dziękuję :)
jaibatrik

możesz również użyć fragment.getClass (). getName (), jeśli chcesz usunąć zmienną klasy i parametr z wywołania
Ben Sewards,

Doskonale współpracuje z "poprzednim ref. TabListener" próbką Androida - tnx. Najnowszy Android „TabListener ref. Sample” [jak na 4 ix 2013] jest naprawdę zły.
Grzegorz Dev

gdzie jest wywołanie metody ft.commit () ??
MSaudi

1
@MuhammadBabar, zobacz stackoverflow.com/questions/23248789/… . Jeśli użyjesz addzamiast replacei obrócisz ekran, będziesz miał wiele fragmentów ” onCreateView().
CoolMind,

26

OK, oto co się dowiedziałem.

Nie rozumiałem, że wszystkie fragmenty, które są dołączone do działania, gdy następuje zmiana konfiguracji (telefon się obraca), są odtwarzane i dodawane z powrotem do działania. (co ma sens)

To, co działo się w konstruktorze TabListener, polegało na odłączaniu karty, jeśli została znaleziona i dołączona do działania. Zobacz poniżej:

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

W dalszej części czynności onCreate wybrano wcześniej wybraną zakładkę z zapisanego stanu instancji. Zobacz poniżej:

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

Po wybraniu karty zostanie ona ponownie dołączona do wywołania zwrotnego 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);
    }
}

Dołączany fragment jest drugim wywołaniem metod onCreateView i onActivityCreated. (Pierwsza to sytuacja, gdy system odtwarza aktywność i wszystkie dołączone fragmenty) Za pierwszym razem pakiet onSavedInstanceState zapisałby dane, ale nie za drugim razem.

Rozwiązaniem jest nie odłączanie fragmentu w konstruktorze TabListener, po prostu pozostawienie go dołączonego. (Nadal musisz go znaleźć w FragmentManager po jego tagu). Ponadto w metodzie onTabSelected sprawdzam, czy fragment został odłączony, zanim go dołączę. Coś takiego:

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
Wspomniane rozwiązania „nie odłączać fragmentu w konstruktorze TabListener” powodują, że fragmenty tabulatora nakładają się na siebie. Widzę zawartość pozostałych fragmentów. Na mnie to nie działa.
Aksel Fatih

@ flock.dux Nie jestem pewien, co masz na myśli, mówiąc o nakładaniu się na siebie. Android dba o ich rozmieszczenie, więc po prostu określamy dołączanie lub odłączanie. Musi być więcej. Może jeśli zadasz nowe pytanie z przykładowym kodem, dowiemy się, co się dzieje.
Dave

1
Miałem ten sam problem (wiele wywołań konstruktora fragmentów z Androida). Twoje odkrycie rozwiązuje mój problem: nie rozumiem, że wszystkie fragmenty, które są dołączone do działania, gdy następuje zmiana konfiguracji (obraca się telefon), są odtwarzane i dodawane z powrotem do działania. (co ma sens)
eugene

26

Miałem ten sam problem z prostym działaniem zawierającym tylko jeden fragment (który czasami był zastępowany). Wtedy zdałem sobie sprawę, że używam onSaveInstanceState tylko we fragmencie (i onCreateView do sprawdzania, czy nie ma saveInstanceState), a nie w działaniu.

Po włączeniu urządzenia działanie zawierające fragmenty zostanie zrestartowane i zostanie wywołane onCreated. Tam załączyłem wymagany fragment (który jest poprawny przy pierwszym uruchomieniu).

Na urządzeniu Android najpierw odtworzył widoczny fragment, a następnie wywołał onCreate czynności zawierającej, do której został dołączony mój fragment, zastępując w ten sposób pierwotnie widoczny.

Aby tego uniknąć, po prostu zmieniłem swoją aktywność, aby sprawdzić zapisany stan instancji:

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
 }

 }

Nie nadpisałem nawet onSaveInstanceState aktywności.


Dziękuję Ci. Pomogło mi to z AppCompatActivity + PreferenceFragmentCompat i zawieszało się podczas wyświetlania okien dialogowych we fragmencie preferencji po zmianie orientacji, ponieważ menedżer fragmentów był zerowy przy tworzeniu drugiego fragmentu.
RoK

12

Dwie przegłosowane odpowiedzi tutaj pokazują rozwiązania dla działania w trybie nawigacji NAVIGATION_MODE_TABS, ale miałem ten sam problem z plikiem NAVIGATION_MODE_LIST. Spowodowało to, że moje fragmenty w niewytłumaczalny sposób straciły swój stan, gdy zmieniła się orientacja ekranu, co było naprawdę denerwujące. Na szczęście dzięki ich pomocnemu kodowi udało mi się to rozgryźć.

Zasadniczo, podczas korzystania z nawigacji po liście, `` 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 () '' ma być wywoływane dwukrotnie!

Zobacz moją onNavigationItemSelected()implementację poniżej.

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

Inspirację do tego rozwiązania zaczerpnąłem stąd .


To rozwiązanie działa w przypadku mojego podobnego problemu z szufladą nawigacji. Znajduję istniejący fragment według identyfikatora i sprawdzam, czy ma tę samą klasę co nowy fragment przed odtworzeniem go.
William

8

Wydaje mi się, że dzieje się tak dlatego, że za każdym razem tworzysz instancję swojego TabListener ... więc system odtwarza twój fragment z saveInstanceState, a następnie robisz to ponownie w swoim onCreate.

Powinieneś owinąć to w if(savedInstanceState == null)tak, aby uruchamiał się tylko wtedy, gdy nie ma pliku saveInstanceState.


Nie sądzę, że to prawda. Kiedy zawijam mój kod addTab w blok if, fragment jest dołączany do działania, ale nie ma żadnych zakładek. Wygląda na to, że za każdym razem musisz dodawać zakładki w metodzie onCreate. Będę dalej przyglądać się temu i publikować więcej, gdy lepiej zrozumiem.
Dave,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.