Fragmento onCreateView y onActivityCreated llamado dos veces

101

Estoy desarrollando una aplicación con ICS y fragmentos de Android 4.0.

Considere este ejemplo modificado de la aplicación de ejemplo de demostración de la API ICS 4.0.3 (nivel de 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;
    }
}

}

Aquí está la salida recuperada de ejecutar este ejemplo y luego girar el teléfono:

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

Mi pregunta es, ¿por qué onCreateView y onActivityCreated se llaman dos veces? ¿La primera vez con un Bundle con el estado guardado y la segunda vez con un SavedInstanceState nulo?

Esto está causando problemas para retener el estado del fragmento en rotación.

Dave
fuente
2
Creo que esta pregunta puede estar relacionada con stackoverflow.com/a/8678705/404395
marioosh

Respuestas:

45

También me estuve rascando la cabeza por esto por un tiempo, y dado que la explicación de Dave es un poco difícil de entender, publicaré mi código (aparentemente funcionando):

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

Como puede ver, es muy parecido a la muestra de Android, aparte de no separarse en el constructor y usar reemplazar en lugar de agregar .

Después de mucho rasguño y prueba y error, descubrí que encontrar el fragmento en el constructor parece hacer que el problema doble de onCreateView desaparezca mágicamente (supongo que termina siendo nulo para onTabSelected cuando se llama a través de la ruta ActionBar.setSelectedNavigationItem () cuando estado de ahorro / restauración).

Staffan
fuente
¡Funciona perfectamente bien! ¡Salvaste mi sueño nocturno! Gracias :)
jaibatrik
también puede usar fragment.getClass (). getName () si desea eliminar la variable de clase y eliminar un parámetro de la llamada
Ben Sewards
Funciona perfectamente con la muestra de Android "ref. TabListener anterior" - tnx. La "muestra de referencia de TabListener" más reciente de Android [como en 4 ix 2013] es realmente incorrecta.
Grzegorz Dev
¿Dónde está la llamada al método ft.commit ()?
MSaudi
1
@MuhammadBabar, consulte stackoverflow.com/questions/23248789/… . Si usa en addlugar de replacey gira la pantalla, tendrá muchos fragmentos ' onCreateView().
CoolMind
26

Ok, esto es lo que descubrí.

Lo que no entendí es que todos los fragmentos que se adjuntan a una actividad cuando ocurre un cambio de configuración (el teléfono gira) se recrean y se vuelven a agregar a la actividad. (que tiene sentido)

Lo que sucedía en el constructor TabListener era que la pestaña se separaba si se encontraba y se adjuntaba a la actividad. Vea abajo:

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

Más adelante, en la actividad de Crear, se seleccionó la pestaña previamente seleccionada del estado de la instancia guardada. Vea abajo:

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

Cuando se selecciona la pestaña, se vuelve a adjuntar en la devolución de llamada 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);
    }
}

El fragmento que se adjunta es la segunda llamada a los métodos onCreateView y onActivityCreated. (El primero es cuando el sistema está recreando la actividad y todos los fragmentos adjuntos) La primera vez que el paquete onSavedInstanceState habría guardado datos, pero no la segunda vez.

La solución es no separar el fragmento en el constructor TabListener, simplemente dejarlo adjunto. (Aún necesita encontrarlo en FragmentManager por su etiqueta) Además, en el método onTabSelected verifico si el fragmento está separado antes de adjuntarlo. Algo como esto:

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);
                }
            }
        }
Dave
fuente
4
Las soluciones mencionadas de "no separar el fragmento en el constructor TabListener" hacen que los fragmentos de las pestañas se superpongan entre sí. Puedo ver el contenido de otros fragmentos. No funciona para mi.
Aksel Fatih
@ flock.dux No estoy seguro de a qué te refieres con superponerse entre sí. Android se encarga de cómo se distribuyen, por lo que solo especificamos adjuntar o desconectar. Debe haber más en juego. Tal vez si hace una nueva pregunta con un código de ejemplo, podemos averiguar qué le está sucediendo.
Dave
1
Tuve el mismo problema (múltiples llamadas al constructor de fragmentos desde Android). Su hallazgo resuelve mi problema: lo que no entendí es que todos los fragmentos que se adjuntan a una actividad cuando ocurre un cambio de configuración (el teléfono gira) se recrean y se vuelven a agregar a la actividad. (lo cual tiene sentido)
eugene
26

He tenido el mismo problema con una actividad simple que lleva solo un fragmento (que a veces se reemplaza). Luego me di cuenta de que uso onSaveInstanceState solo en el fragmento (y onCreateView para verificar si hay SavedInstanceState), no en la actividad.

Al encender el dispositivo, la actividad que contiene los fragmentos se reinicia y se llama a onCreated. Allí adjunté el fragmento requerido (que es correcto en el primer inicio).

En el dispositivo, gire Android primero recreó el fragmento que era visible y luego llamó a onCreate de la actividad contenedora donde se adjuntó mi fragmento, reemplazando así el visible original.

Para evitar eso, simplemente cambié mi actividad para verificar si había guardadoInstanceState:

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
 }

 }

Ni siquiera sobrescribí onSaveInstanceState de la actividad.

Gunnar Bernstein
fuente
Gracias. Me ayudó con AppCompatActivity + PreferenceFragmentCompat y se bloqueó al mostrar cuadros de diálogo en el fragmento de preferencia después del cambio de orientación, ya que el administrador de fragmentos era nulo en la creación del segundo fragmento.
RoK
12

Las dos respuestas votadas aquí muestran soluciones para una actividad con modo de navegación NAVIGATION_MODE_TABS, pero tuve el mismo problema con un NAVIGATION_MODE_LIST. Hizo que mis Fragmentos perdieran inexplicablemente su estado cuando la orientación de la pantalla cambió, lo cual fue realmente molesto. Afortunadamente, gracias a su código útil, logré descifrarlo.

Básicamente, cuando se usa una navegación de lista, `` 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 () `para ser llamado dos veces!

Vea mi onNavigationItemSelected()implementación a continuación.

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

Tomé prestada inspiración para esta solución de aquí .

XåpplI'-I0llwlg'I -
fuente
Esta solución funciona para mi problema similar con un cajón de navegación. Encuentro el fragmento existente por ID y verifico si tiene la misma clase que el nuevo fragmento antes de recrearlo.
William
8

Me parece que se debe a que está creando una instancia de su TabListener cada vez ... por lo que el sistema está recreando su fragmento de SavedInstanceState y luego lo está haciendo nuevamente en su onCreate.

Debe envolver eso en un if(savedInstanceState == null)modo que solo se active si no hay SavedInstanceState.

Barak
fuente
No creo que eso sea correcto. Cuando envuelvo mi código addTab en el bloque if, el fragmento se adjunta a la actividad pero no hay pestañas. Parece que debe agregar las pestañas cada vez en el método onCreate. Seguiré investigando esto y publicaré más a medida que entienda mejor.
Dave