¿Cómo guardar correctamente el estado de instancia de Fragments en el back stack?

489

He encontrado muchas instancias de una pregunta similar sobre SO, pero desafortunadamente ninguna respuesta cumple con mis requisitos.

Tengo diferentes diseños para retrato y paisaje y estoy usando back stack, lo que me impide usar setRetainState()y trucos usando rutinas de cambio de configuración.

Muestro cierta información al usuario en TextViews, que no se guardan en el controlador predeterminado. Al escribir mi solicitud únicamente con Actividades, lo siguiente funcionó bien:

TextView vstup;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.whatever);
    vstup = (TextView)findViewById(R.id.whatever);
    /* (...) */
}

@Override
public void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    state.putCharSequence(App.VSTUP, vstup.getText());
}

@Override
public void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    vstup.setText(state.getCharSequence(App.VSTUP));
}

Con Fragments, esto funciona solo en situaciones muy específicas. Específicamente, lo que se rompe horriblemente es reemplazar un fragmento, colocarlo en la pila posterior y luego girar la pantalla mientras se muestra el nuevo fragmento. Por lo que entendí, el antiguo fragmento no recibe una llamada onSaveInstanceState()cuando se reemplaza, pero permanece vinculado de alguna manera al Activityy este método se llama más tarde cuando Viewya no existe, por lo que busca cualquiera de mis TextViewresultados en a NullPointerException.

Además, descubrí que mantener la referencia a mi TextViewsno es una buena idea con Fragments, incluso si estaba bien con Activitys. En ese caso, en onSaveInstanceState()realidad guarda el estado, pero el problema vuelve a aparecer si giro la pantalla dos veces cuando el fragmento está oculto, ya onCreateView()que no se llama en la nueva instancia.

Pensé en salvar el estado en el onDestroyView()en algún Bundleelemento de miembro de la clase de tipo (en realidad es más datos, no sólo uno TextView) y el ahorro que en onSaveInstanceState()pero hay otros inconvenientes. Principalmente, si el fragmento se muestra actualmente, el orden de invocar las dos funciones se invierte, por lo que necesitaría tener en cuenta dos situaciones diferentes. ¡Debe haber una solución más limpia y correcta!

El vee
fuente
1
Aquí hay un muy buen ejemplo con una explicación detallada también. emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam
1
Secundo el enlace emunee.com. ¡Resolvió un problema de UI de palo para mí!
Reenactor Rob

Respuestas:

541

Para guardar correctamente el estado de la instancia Fragment, debe hacer lo siguiente:

1. En el fragmento, guarde el estado de instancia anulando onSaveInstanceState()y restaurando en onActivityCreated():

class MyFragment extends Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's state here
        }
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's state here
    }

}

2. Y un punto importante , en la actividad, debe guardar la instancia del fragmento onSaveInstanceState()y restaurarla onCreate().

class MyActivity extends Activity {

    private MyFragment 

    public void onCreate(Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's instance
            mMyFragment = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
            ...
        }
        ...
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's instance
        getSupportFragmentManager().putFragment(outState, "myFragmentName", mMyFragment);
    }

}

Espero que esto ayude.

ThanhHH
fuente
77
¡Esto funcionó perfectamente para mí! Sin soluciones alternativas, sin hacks, simplemente tiene sentido de esta manera. Gracias por esto, hizo horas de búsqueda exitosas. SaveInstanceState () de sus valores en fragmento, luego guarde el fragmento en la Actividad que contiene el fragmento, luego restaure :)
MattMatt
77
¿Qué es mContent?
wizurd
14
@wizurd mContent es un Fragment, es una referencia a la instancia del fragmento actual en la actividad.
ThanhHH
13
¿Puede explicar cómo esto salvará el estado de instancia de un fragmento en la pila de respaldo? Eso es lo que preguntó el OP.
hitmaneidos
51
Esto no está relacionado con la pregunta, no se llama a onSaveInstance cuando el fragmento se coloca en el backstack
Tadas Valaitis
87

Esta es la forma en que estoy usando en este momento ... es muy complicado, pero al menos maneja todas las situaciones posibles. En caso de que alguien esté interesado.

public final class MyFragment extends Fragment {
    private TextView vstup;
    private Bundle savedState = null;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.whatever, null);
        vstup = (TextView)v.findViewById(R.id.whatever);

        /* (...) */

        /* If the Fragment was destroyed inbetween (screen rotation), we need to recover the savedState first */
        /* However, if it was not, it stays in the instance from the last onDestroyView() and we don't want to overwrite it */
        if(savedInstanceState != null && savedState == null) {
            savedState = savedInstanceState.getBundle(App.STAV);
        }
        if(savedState != null) {
            vstup.setText(savedState.getCharSequence(App.VSTUP));
        }
        savedState = null;

        return v;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        savedState = saveState(); /* vstup defined here for sure */
        vstup = null;
    }

    private Bundle saveState() { /* called either from onDestroyView() or onSaveInstanceState() */
        Bundle state = new Bundle();
        state.putCharSequence(App.VSTUP, vstup.getText());
        return state;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /* If onDestroyView() is called first, we can use the previously savedState but we can't call saveState() anymore */
        /* If onSaveInstanceState() is called first, we don't have savedState, so we need to call saveState() */
        /* => (?:) operator inevitable! */
        outState.putBundle(App.STAV, (savedState != null) ? savedState : saveState());
    }

    /* (...) */

}

Alternativamente , siempre es posible mantener los datos mostrados en Views pasivos en variables y usar los Views solo para mostrarlos, manteniendo las dos cosas sincronizadas. Sin embargo, no considero que la última parte esté muy limpia.

El vee
fuente
68
Esta es la mejor solución que he encontrado hasta ahora, pero todavía queda un problema (algo exótico): si tiene dos fragmentos Ay B, donde Aestá actualmente en la pila y Bes visible, entonces pierde el estado de A(lo invisible uno) si gira la pantalla dos veces . El problema es que onCreateView()no se llama solo en este escenario onCreate(). Entonces, más tarde, onSaveInstanceState()no hay vistas para salvar el estado. Uno tendría que almacenar y luego guardar el estado pasado onCreate().
devconsole
77
@devconsole ¡Desearía poder darte 5 votos para este comentario! Esta rotación dos veces me ha estado matando durante días.
DroidT
Gracias por la gran respuesta! Aunque tengo una pregunta. ¿Dónde está el mejor lugar para instanciar el objeto modelo (POJO) en este fragmento?
Renjith
77
Para ayudar a ahorrar tiempo para los demás, App.VSTUPy App.STAVambas son etiquetas de cadena que representan los objetos que intentan obtener. Ejemplo: savedState = savedInstanceState.getBundle(savedGamePlayString);osavedState.getDouble("averageTime")
Tanner Hallman el
1
Esto es una cosa de belleza.
Ivan
61

En la última biblioteca de soporte, ninguna de las soluciones discutidas aquí es necesaria. Puedes jugar con tus Activityfragmentos como quieras usando el FragmentTransaction. Solo asegúrese de que sus fragmentos puedan identificarse con una identificación o etiqueta.

Los fragmentos se restaurarán automáticamente siempre que no intentes recrearlos en cada llamada a onCreate(). En su lugar, debe verificar si savedInstanceStateno es nulo y encontrar las referencias antiguas a los fragmentos creados en este caso.

Aquí hay un ejemplo:

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

    if (savedInstanceState == null) {
        myFragment = MyFragment.newInstance();
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.my_container, myFragment, MY_FRAGMENT_TAG)
                .commit();
    } else {
        myFragment = (MyFragment) getSupportFragmentManager()
                .findFragmentByTag(MY_FRAGMENT_TAG);
    }
...
}

Sin embargo, tenga en cuenta que actualmente hay un error al restaurar el estado oculto de un fragmento. Si está ocultando fragmentos en su actividad, deberá restaurar este estado manualmente en este caso.

Ricardo
fuente
2
¿Es esto algo que notó al usar la biblioteca de soporte o lo leyó en alguna parte? ¿Hay más información que pueda proporcionar al respecto? ¡Gracias!
Piovezan
1
@Piovezan puede deducirse implícitamente de los documentos. Por ejemplo, el documento beginTransaction () dice lo siguiente: "Esto se debe a que el marco se encarga de guardar sus fragmentos actuales en el estado (...)". También he codificado mis aplicaciones con este comportamiento esperado desde hace bastante tiempo.
Ricardo
1
@Ricardo, ¿se aplica esto si utiliza un ViewPager?
Derek Beattie
1
Normalmente sí, a menos que haya cambiado el comportamiento predeterminado en su implementación de FragmentPagerAdaptero FragmentStatePagerAdapter. Si observa el código de FragmentStatePagerAdapter, por ejemplo, verá que el restoreState()método restaura los fragmentos del FragmentManagerparámetro que pasó al crear el adaptador.
Ricardo
44
Creo que esta contribución es la mejor respuesta a la pregunta original. También es el que, en mi opinión, está mejor alineado con el funcionamiento de la plataforma Android. Recomendaría marcar esta respuesta como la "Aceptada" para ayudar mejor a los futuros lectores.
dbm
17

Solo quiero dar la solución que se me ocurrió que maneja todos los casos presentados en esta publicación que obtuve de Vasek y devconsole. Esta solución también maneja el caso especial cuando el teléfono se gira más de una vez mientras los fragmentos no son visibles.

Aquí es donde guardo el paquete para su uso posterior ya que onCreate y onSaveInstanceState son las únicas llamadas que se realizan cuando el fragmento no es visible

MyObject myObject;
private Bundle savedState = null;
private boolean createdStateInDestroyView;
private static final String SAVED_BUNDLE_TAG = "saved_bundle";

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        savedState = savedInstanceState.getBundle(SAVED_BUNDLE_TAG);
    }
}

Dado que destroyView no se llama en la situación de rotación especial, podemos estar seguros de que si crea el estado, deberíamos usarlo.

@Override
public void onDestroyView() {
    super.onDestroyView();
    savedState = saveState();
    createdStateInDestroyView = true;
    myObject = null;
}

Esta parte sería la misma.

private Bundle saveState() { 
    Bundle state = new Bundle();
    state.putSerializable(SAVED_BUNDLE_TAG, myObject);
    return state;
}

Ahora aquí está la parte difícil. En mi método onActivityCreated, ejemplifico la variable "myObject", pero la rotación ocurre en onActivity y onCreateView no reciben llamadas. Por lo tanto, myObject será nulo en esta situación cuando la orientación gire más de una vez. Lo soluciono reutilizando el mismo paquete que se guardó en onCreate que el paquete saliente.

    @Override
public void onSaveInstanceState(Bundle outState) {

    if (myObject == null) {
        outState.putBundle(SAVED_BUNDLE_TAG, savedState);
    } else {
        outState.putBundle(SAVED_BUNDLE_TAG, createdStateInDestroyView ? savedState : saveState());
    }
    createdStateInDestroyView = false;
    super.onSaveInstanceState(outState);
}

Ahora, donde quiera restaurar el estado, simplemente use el paquete savedState

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}
DroidT
fuente
¿Me puede decir ... ¿Qué es "MyObject" aquí?
kavie
2
Cualquier cosa que quieras que sea. Es solo un ejemplo que representa algo que se guardaría en el paquete.
DroidT
3

Gracias a DroidT , hice esto:

Me doy cuenta de que si el Fragment no se ejecuta en onCreateView (), su vista no se instancia. Entonces, si el fragmento en la pila posterior no creó sus vistas, guardo el último estado almacenado, de lo contrario, construyo mi propio paquete con los datos que quiero guardar / restaurar.

1) Amplíe esta clase:

import android.os.Bundle;
import android.support.v4.app.Fragment;

public abstract class StatefulFragment extends Fragment {

    private Bundle savedState;
    private boolean saved;
    private static final String _FRAGMENT_STATE = "FRAGMENT_STATE";

    @Override
    public void onSaveInstanceState(Bundle state) {
        if (getView() == null) {
            state.putBundle(_FRAGMENT_STATE, savedState);
        } else {
            Bundle bundle = saved ? savedState : getStateToSave();

            state.putBundle(_FRAGMENT_STATE, bundle);
        }

        saved = false;

        super.onSaveInstanceState(state);
    }

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);

        if (state != null) {
            savedState = state.getBundle(_FRAGMENT_STATE);
        }
    }

    @Override
    public void onDestroyView() {
        savedState = getStateToSave();
        saved = true;

        super.onDestroyView();
    }

    protected Bundle getSavedState() {
        return savedState;
    }

    protected abstract boolean hasSavedState();

    protected abstract Bundle getStateToSave();

}

2) En tu Fragmento, debes tener esto:

@Override
protected boolean hasSavedState() {
    Bundle state = getSavedState();

    if (state == null) {
        return false;
    }

    //restore your data here

    return true;
}

3) Por ejemplo, puede llamar a hasSavedState en onActivityCreated:

@Override
public void onActivityCreated(Bundle state) {
    super.onActivityCreated(state);

    if (hasSavedState()) {
        return;
    }

    //your code here
}
Noturno
fuente
-6
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();
Nilesh Savaliya
fuente
1
No relacionado con la pregunta original.
Jorge E. Hernández