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

He encontrado muchos casos de una pregunta similar sobre SO pero ninguna respuesta por desgracia cumple mis requisitos.

Tengo diferentes diseños para el retrato y el paisaje y estoy usando pila trasera, que tanto me impide usar setRetainState() y trucos utilizando rutinas de cambio de configuración.

Yo muestro cierta información para el usuario en TextViews, que no se guardan en el controlador predeterminado. Al escribir mi solicitud usando únicamente Actividades, 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 sólo funciona en situaciones muy específicas. Específicamente, lo que se rompe horriblemente es reemplazar un fragmento, ponerlo en la pila trasera y luego girar la pantalla mientras se muestra el nuevo fragmento. De lo que he entendido, el fragmento antiguo no recibe una llamada a onSaveInstanceState() cuando se sustituye, pero permanece vinculado de alguna manera a la Activity y este método se llama más tarde cuando su View ya no existe, así que busca cualquiera de mis resultados de TextView s En una NullPointerException .

Además, encontré que mantener la referencia a mis TextViews no es una buena idea con Fragment s, aunque estuviera bien con Activity 's. En ese caso, onSaveInstanceState() realmente guarda el estado pero el problema vuelve a aparecer si roto la pantalla dos veces cuando el fragmento está oculto, ya que su onCreateView() no se llama en la nueva instancia.

Pensé en guardar el estado en onDestroyView() en algún elemento de clase Bundle elemento miembro (es en realidad más datos, no sólo un TextView ) y guardar que en onSaveInstanceState() pero hay otros inconvenientes. Principalmente, si el fragmento se muestra en la actualidad, el orden de llamar a 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 la instancia de Fragment debe hacer lo siguiente:

1. En el fragmento, guarde el estado de la instancia invalidando onSaveInstanceState() y restaurar 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 punto importante , en la actividad, tienes que 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 se encarga de 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()); } /* (...) */ } 

De forma alternativa , siempre es posible mantener los datos mostrados en las View pasivas en variables y usar las View sólo para mostrarlas, manteniendo las dos cosas sincronizadas. No considero la última parte muy limpia, sin embargo.

En la última biblioteca de soporte, ninguna de las soluciones discutidas aquí son necesarias. Puedes jugar con los fragmentos de tu Activity como quieras usando FragmentTransaction . Sólo asegúrese de que sus fragmentos se pueden identificar con una identificación o una etiqueta.

Los fragmentos se restaurarán automáticamente siempre que no intente recrearlos en cada llamada a onCreate() . En su lugar, debe comprobar si savedInstanceState no 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); } ... } 

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

Sólo quiero dar la solución que me surgió que maneja todos los casos presentados en este post que he derivado 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í es donde se almacena 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 debemos 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 instanciar la variable "myObject" pero la rotación pasa onActivity y onCreateView no se llama. Por lo tanto, myObject será nulo en esta situación cuando la orientación gira más de una vez. Consigo alrededor de esto reusando el mismo paquete que fue ahorrado en onCreate como el paquete hacia fuera que va.

  @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 que desee restaurar el estado, 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 es instanciada. Por lo tanto, si el fragmento de la pila trasera no creó sus vistas, guardo el último estado almacenado, de lo contrario construyo mi propio paquete con los datos que quiero guardar / restaurar.

1) Ampliar 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 } 
 final FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.hide(currentFragment); ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile"); ft.addToBackStack(null); ft.commit(); 
  • Fragmentos superpuestos con DrawerLayout / NavigationView
  • Cómo resolver un error: getSharedPreferences (String, int) no está definido para el tipo nuevo View.OnClickListener () {}
  • DialogFragment bloquea la aplicación en el cambio de orientación
  • A veces el fragmento pierde actividad
  • Fragmentos no visibles después de cambiar tabulación en FragmentTabHost anidado
  • NotifyDataSetChanged () en Fragmento no está actualizando listview
  • FragmentPagerAdapter no se puede resolver con un tipo
  • androide viewpager dentro de otro viewpager
  • No se puede extraer del fragmento
  • Android - Establecer id de fragmento
  • SDK 5.0 RecyclerView no se puede instanciar
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.