Una vez por todas, ¿cómo guardar correctamente el estado de la instancia de Fragmentos en la stack posterior?

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

Tengo diferentes diseños para retrato y paisaje y estoy usando la stack trasera, 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 guarda en el controlador predeterminado. Al escribir mi aplicación utilizando únicamente 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 Fragment s, esto funciona solo en situaciones muy específicas. Específicamente, lo que se rompe horriblemente es reemplazar un fragmento, colocarlo en la stack posterior y luego girar la pantalla mientras se muestra el nuevo fragmento. Según lo que entendí, el fragmento anterior no recibe una llamada a onSaveInstanceState() cuando se reemplaza, pero permanece vinculado de alguna manera a la Activity y este método se llama más tarde cuando su View ya no existe, por lo que busco cualquiera de los resultados de mi TextView . en una NullPointerException .

Además, descubrí que mantener la referencia a mis TextViews no es una buena idea con Fragment s, incluso si estaba bien con Activity ‘s. En ese caso, onSaveInstanceState() realmente guarda el estado, pero el problema reaparece si giro la pantalla dos veces cuando el fragmento está oculto, ya que onCreateView() no se llama en la nueva instancia.

Pensé en guardar el estado en onDestroyView() en algún elemento miembro de la clase de tipo Bundle (en realidad es más datos, no solo un TextView ) y guardarlo en onSaveInstanceState() pero hay otros inconvenientes. Principalmente, si el fragmento se muestra actualmente, el orden de invocación de las dos funciones se invierte, por lo que tendría que dar cuenta de dos situaciones diferentes. ¡Debe haber una solución más limpia y correcta!

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

1. En el fragmento, guarde el estado de la instancia sobrescribiendo onSaveInstanceState() y restaure en onActivityCreated() :

 @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 en onSaveInstanceState() y restaurar en onCreate() .

 public void onCreate(Bundle savedInstanceState) { ... if (savedInstanceState != null) { //Restore the fragment's instance mContent = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName"); ... } ... } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); //Save the fragment's instance getSupportFragmentManager().putFragment(outState, "myFragmentName", mContent); } 

Espero que esto ayude.

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 visualizados en las View pasivas en las variables y usar las View solo para mostrarlos, manteniendo las dos cosas sincronizadas. Aunque no considero la última parte muy limpia.

En la última biblioteca de soporte, ninguna de las soluciones discutidas aquí es necesaria. Puedes jugar con los fragmentos de tu Activity como quieras usando FragmentTransaction . Solo asegúrate de que tus fragmentos se puedan identificar con una identificación o etiqueta.

Los fragmentos se restaurarán automáticamente siempre que no intente recrearlos en cada llamada a onCreate() . En su lugar, debe verificar si savedInstanceState no es nulo y encontrar las referencias anteriores 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.

Solo quiero dar la solución que surgió 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 que los fragmentos no son visibles.

Aquí está el almacenamiento del paquete para su uso posterior, ya que onCreate y onSaveInstanceState son las únicas llamadas que se realizan cuando el fragmento no está 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); } } 

Como no se llama a destroyView 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 lo mismo.

 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 instalo la variable “myObject” pero la rotación ocurre enActivity y onCreateView no se llama. 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 como 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); } ... } 

Gracias a DroidT , hice esto:

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

1) Extiende 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; } //restre 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 } 
 final FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.hide(currentFragment); ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile"); ft.addToBackStack(null); ft.commit();