Fragment onCreateView e onActivityCreated chiamati due volte


101

Sto sviluppando un'app utilizzando Android 4.0 ICS e fragments.

Considera questo esempio modificato dall'app di esempio demo dell'API ICS 4.0.3 (livello 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;
    }
}

}

Ecco l'output recuperato dall'esecuzione di questo esempio e dalla rotazione del telefono:

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

La mia domanda è: perché onCreateView e onActivityCreated vengono chiamati due volte? La prima volta con un bundle con lo stato salvato e la seconda volta con un savedInstanceState nullo?

Ciò causa problemi con il mantenimento dello stato del frammento durante la rotazione.


2
Credo che questa domanda può essere correlato a stackoverflow.com/a/8678705/404395
marioosh

Risposte:


45

Mi stavo anche grattando la testa per un po ', e poiché la spiegazione di Dave è un po' difficile da capire, posterò il mio codice (apparentemente funzionante):

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

Come puoi vedere, è praticamente simile all'esempio di Android, a parte il non scollegamento nel costruttore e l'utilizzo di sostituzione anziché di aggiunta .

Dopo molti headcratching e tentativi, ho scoperto che trovare il frammento nel costruttore sembra far sparire magicamente il doppio problema di onCreateView (presumo che finisca per essere nullo per onTabSelected quando viene chiamato tramite il percorso ActionBar.setSelectedNavigationItem () quando stato di salvataggio / ripristino).


Funziona perfettamente bene! Hai salvato la mia notte di sonno! Grazie :)
jaibatrik

puoi anche usare fragment.getClass (). getName () se vuoi rimuovere la variabile di classe e rimuovere un parametro dalla chiamata
Ben Sewards

Funziona perfettamente con il campione Android "precedente ref. TabListener" - tnx. Il più recente "TabListener ref. Sample" di Android [come il 4 ix 2013] è davvero, davvero sbagliato.
Grzegorz Dev

dov'è la chiamata al metodo ft.commit () ??
MSaudi

1
@MuhammadBabar, vedi stackoverflow.com/questions/23248789/... . Se usi addinvece di replacee ruoti lo schermo, avrai molti frammenti ' onCreateView().
CoolMind

26

Ok, ecco cosa ho scoperto.

Quello che non ho capito è che tutti i frammenti allegati a un'attività quando si verifica una modifica alla configurazione (il telefono ruota) vengono ricreati e aggiunti all'attività. (che ha senso)

Quello che stava accadendo nel costruttore TabListener era che la scheda veniva scollegata se veniva trovata e allegata all'attività. Vedi sotto:

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

Successivamente, nell'attività su Crea, la scheda selezionata in precedenza è stata selezionata dallo stato dell'istanza salvata. Vedi sotto:

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

Quando la scheda è stata selezionata, verrà ricollegata nella richiamata 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);
    }
}

Il frammento allegato è la seconda chiamata ai metodi onCreateView e onActivityCreated. (Il primo è quando il sistema sta ricreando l'attività e tutti i frammenti allegati) La prima volta che il bundle onSavedInstanceState avrebbe salvato i dati ma non la seconda volta.

La soluzione è non scollegare il frammento nel costruttore TabListener, ma lasciarlo allegato. (Devi ancora trovarlo nel FragmentManager tramite il suo tag) Inoltre, nel metodo onTabSelected controllo per vedere se il frammento è staccato prima di collegarlo. Qualcosa come questo:

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
Le soluzioni citate "per non scollegare il frammento nel costruttore TabListener" fanno sì che i frammenti di tabulazione si sovrappongano. Posso vedere il contenuto degli altri frammenti. Non funziona per me.
Aksel Fatih

@ flock.dux Non sono sicuro di cosa intendi per sovrapposizione tra loro. Android si occupa di come sono disposti, quindi è sufficiente specificare allegare o scollegare. Dev'esserci di più in corso. Forse se fai una nuova domanda con un codice di esempio possiamo capire cosa sta succedendo per te.
Dave

1
Ho avuto lo stesso problema (più chiamate al costruttore di frammenti da Android). La tua scoperta risolve il mio problema: quello che non ho capito è che tutti i frammenti allegati a un'attività quando si verifica una modifica della configurazione (il telefono ruota) vengono ricreati e aggiunti all'attività. (il che ha senso)
eugene

26

Ho avuto lo stesso problema con un'attività semplice che trasportava un solo frammento (che a volte veniva sostituito). Mi sono quindi reso conto di utilizzare onSaveInstanceState solo nel frammento (e onCreateView per verificare la presenza di savedInstanceState), non nell'attività.

All'accensione del dispositivo viene riavviata l'attività contenente i frammenti e viene chiamato onCreated. Lì ho allegato il frammento richiesto (che è corretto al primo avvio).

Sul dispositivo accendere Android prima ricreato il frammento che era visibile e poi chiamato onCreate dell'attività contenitore a cui era attaccato il mio frammento, sostituendo così quello visibile originale.

Per evitare ciò, ho semplicemente cambiato la mia attività per verificare la presenza di savedInstanceState:

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
 }

 }

Non ho nemmeno sovrascritto suSaveInstanceState dell'attività.


Grazie. Mi ha aiutato con AppCompatActivity + PreferenceFragmentCompat e l'arresto anomalo durante la visualizzazione delle finestre di dialogo nel frammento di preferenza dopo la modifica dell'orientamento, poiché il gestore dei frammenti era nullo alla creazione del secondo frammento.
RoK

12

Le due risposte votate qui mostrano soluzioni per un'attività con modalità di navigazione NAVIGATION_MODE_TABS, ma ho avuto lo stesso problema con un file NAVIGATION_MODE_LIST. Ha causato la perdita inspiegabile dei miei frammenti quando l'orientamento dello schermo è cambiato, il che è stato davvero fastidioso. Per fortuna, grazie al loro codice utile sono riuscito a capirlo.

Fondamentalmente, quando si utilizza una navigazione a elenco, `` 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 () `da chiamare due volte!

Vedi la mia onNavigationItemSelected()implementazione di seguito.

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

Ho preso in prestito l'ispirazione per questa soluzione da qui .


Questa soluzione funziona per il mio problema simile con un cassetto di navigazione. Trovo il frammento esistente in base all'ID e controllo se ha la stessa classe del nuovo frammento prima di ricrearlo.
William

8

Mi sembra che sia perché stai istanziando il tuo TabListener ogni volta ... quindi il sistema sta ricreando il tuo frammento da savedInstanceState e poi lo stai facendo di nuovo nel tuo onCreate.

Dovresti racchiuderlo in un in if(savedInstanceState == null)modo che venga attivato solo se non è presente saveInstanceState.


Non credo sia corretto. Quando inserisco il mio codice addTab nel blocco if, il frammento viene allegato all'attività ma non ci sono schede. Sembra che tu debba aggiungere le schede ogni volta nel metodo onCreate. Continuerò a esaminare questo aspetto e ne posterò di più se capisco meglio.
Dave,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.