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




fragments tutorial (6)

He encontrado muchos ejemplos de una pregunta similar en SO, pero desafortunadamente ninguna respuesta cumple con mis requisitos.

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

Le muestro cierta información al usuario en TextViews, que no se guardan en el controlador predeterminado. Al escribir mi solicitud únicamente utilizando 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 pila posterior y luego girar la pantalla mientras se muestra el nuevo fragmento. Por lo que entendí, el fragmento antiguo 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 adelante cuando su View ya no existe, por lo que busco cualquiera de los resultados de TextView . en una NullPointerException .

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

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


Answers

En la última biblioteca de soporte, ninguna de las soluciones aquí discutidas es necesaria. Puedes jugar con los fragmentos de tu Activity tu gusto usando FragmentTransaction . Solo asegúrese de que sus fragmentos puedan identificarse con un ID o etiqueta.

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

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


final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();

Gracias a DroidT , hice esto:

Me doy cuenta de que si el Fragmento no se ejecuta enCreateView (), su vista no está instanciada. Entonces, si el fragmento en 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) 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;
    }

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

Solo quiero dar la solución que se me ocurrió para manejar todos los casos presentados en este post 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í es donde almaceno 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, he creado una instancia de la variable "myObject", pero la rotación ocurre en Activity y onCreateView no recibe llamadas. Por lo tanto, myObject será nulo en esta situación cuando la orientación rote más de una vez. Evito esto 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 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);
    }
    ...
}

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

1. En el fragmento, guarde el estado de la instancia al anular 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 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.


Solución perfecta que encuentra un fragmento antiguo en la pila y lo carga si existe en la pila.

/**
     * replace or add fragment to the container
     *
     * @param fragment pass android.support.v4.app.Fragment
     * @param bundle pass your extra bundle if any
     * @param popBackStack if true it will clear back stack
     * @param findInStack if true it will load old fragment if found
     */
    public void replaceFragment(Fragment fragment, @Nullable Bundle bundle, boolean popBackStack, boolean findInStack) {
        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();
        String tag = fragment.getClass().getName();
        Fragment parentFragment;
        if (findInStack && fm.findFragmentByTag(tag) != null) {
            parentFragment = fm.findFragmentByTag(tag);
        } else {
            parentFragment = fragment;
        }
        // if user passes the @bundle in not null, then can be added to the fragment
        if (bundle != null)
            parentFragment.setArguments(bundle);
        else parentFragment.setArguments(null);
        // this is for the very first fragment not to be added into the back stack.
        if (popBackStack) {
            fm.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        } else {
            ft.addToBackStack(parentFragment.getClass().getName() + "");
        }
        ft.replace(R.id.contenedor_principal, parentFragment, tag);
        ft.commit();
        fm.executePendingTransactions();
    }

usarlo como

Fragment f = new YourFragment();
replaceFragment(f, null, boolean true, true); 




android android-fragments