android - programacion - ¿Cómo puedo mantener el estado del fragmento cuando se agrega a la pila posterior?
manual de programacion android pdf (12)
He escrito una actividad ficticia que alterna entre dos fragmentos. Cuando pasas de FragmentA a FragmentB, FragmentA se agrega a la pila posterior. Sin embargo, cuando regreso a FragmentA (presionando hacia atrás), se crea un FragmentA totalmente nuevo y se pierde el estado en que se encontraba. Tengo la sensación de que estoy buscando lo mismo que this pregunta, pero he incluido un ejemplo de código completo para ayudar a eliminar el problema:
public class FooActivity extends Activity {
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(android.R.id.content, new FragmentA());
transaction.commit();
}
public void nextFragment() {
final FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.replace(android.R.id.content, new FragmentB());
transaction.addToBackStack(null);
transaction.commit();
}
public static class FragmentA extends Fragment {
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
final View main = inflater.inflate(R.layout.main, container, false);
main.findViewById(R.id.next_fragment_button).setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
((FooActivity) getActivity()).nextFragment();
}
});
return main;
}
@Override public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save some state!
}
}
public static class FragmentB extends Fragment {
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.b, container, false);
}
}
}
Con algunos mensajes de registro agregados:
07-05 14:28:59.722 D/OMG ( 1260): FooActivity.onCreate
07-05 14:28:59.742 D/OMG ( 1260): FragmentA.onCreateView
07-05 14:28:59.742 D/OMG ( 1260): FooActivity.onResume
<Tap Button on FragmentA>
07-05 14:29:12.842 D/OMG ( 1260): FooActivity.nextFragment
07-05 14:29:12.852 D/OMG ( 1260): FragmentB.onCreateView
<Tap ''Back''>
07-05 14:29:16.792 D/OMG ( 1260): FragmentA.onCreateView
Nunca llama a FragmentA.onSaveInstanceState y crea un nuevo FragmentA cuando respondes. Sin embargo, si estoy en FragmentA y bloqueo la pantalla, se llama a FragmentA.onSaveInstanceState. Tan extraño ... ¿me equivoco al esperar que un fragmento agregado a la pila de respaldo no necesite recreación? Esto es lo que dicen los docs :
Mientras que, si llama a addToBackStack () al eliminar un fragmento, entonces el fragmento se detiene y se reanudará si el usuario navega hacia atrás.
primero : simplemente use el método add en lugar de reemplazar el método de la clase FragmentTransaction, luego debe agregar secondFragment para apilar mediante el método addToBackStack
segundo : al hacer clic atrás tienes que llamar a popBackStackImmediate ()
Fragment sourceFragment = new SourceFragment ();
final Fragment secondFragment = new SecondFragment();
final FragmentTransaction ft = getChildFragmentManager().beginTransaction();
ft.add(R.id.child_fragment_container, secondFragment );
ft.hide(sourceFragment );
ft.addToBackStack(NewsShow.class.getName());
ft.commit();
((SecondFragment)secondFragment).backFragmentInstanceClick = new SecondFragment.backFragmentNewsResult()
{
@Override
public void backFragmentNewsResult()
{
getChildFragmentManager().popBackStackImmediate();
}
};
Aquí, ya que onSaveInstanceState
en fragmento no llama cuando agrega fragmentos en backstack. El ciclo de vida del fragmento en backstack cuando se restaura comienza en onCreateView
y termina en onDestroyView
mientras que onSaveInstanceState
se llama entre onDestroyView
y onDestroy
. Mi solución es crear variable de instancia e init en onCreate
. Código de muestra:
private boolean isDataLoading = true;
private ArrayList<String> listData;
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
isDataLoading = false;
// init list at once when create fragment
listData = new ArrayList();
}
Y compruébalo onActivityCreated
:
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if(isDataLoading){
fetchData();
}else{
//get saved instance variable listData()
}
}
private void fetchData(){
// do fetch data into listData
}
Comparando con el UINavigationController
de Apple y UIViewController
, Google no funciona bien en la arquitectura de software de Android. Y el documento de Android sobre Fragment
no ayuda mucho.
Cuando ingresa FragmentB desde FragmentA, la instancia FragmentA existente no se destruye. Cuando presiona Atrás en FragmentB y regresa a FragmentA, no creamos una nueva instancia de FragmentA. Se onCreateView()
la instancia de FragmentA existente onCreateView()
.
La clave es que no deberíamos inflar de nuevo la vista en onCreateView()
FragmentA, porque estamos usando la instancia existente de FragmentA. Necesitamos guardar y reutilizar el rootView.
El siguiente código funciona bien. No solo mantiene el estado del fragmento, sino que también reduce la carga de la RAM y la CPU (porque solo inflemos el diseño si es necesario). No puedo creer que el código de muestra y el documento de Google nunca lo mencionen, pero siempre inflan el diseño .
Versión 1 (No use la versión 1. Use la versión 2)
public class FragmentA extends Fragment {
View _rootView;
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (_rootView == null) {
// Inflate the layout for this fragment
_rootView = inflater.inflate(R.layout.fragment_a, container, false);
// Find and setup subviews
_listView = (ListView)_rootView.findViewById(R.id.listView);
...
} else {
// Do not inflate the layout again.
// The returned View of onCreateView will be added into the fragment.
// However it is not allowed to be added twice even if the parent is same.
// So we must remove _rootView from the existing parent view group
// (it will be added back).
((ViewGroup)_rootView.getParent()).removeView(_rootView);
}
return _rootView;
}
}
------ Actualización el 3 de mayo de 2005: -------
Como mencionan los comentarios, a veces _rootView.getParent()
es nulo en onCreateView
, lo que provoca el bloqueo. La versión 2 elimina _rootView en onDestroyView (), como se sugirió dell116. Probado en Android 4.0.3, 4.4.4, 5.1.0.
Versión 2
public class FragmentA extends Fragment {
View _rootView;
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
if (_rootView == null) {
// Inflate the layout for this fragment
_rootView = inflater.inflate(R.layout.fragment_a, container, false);
// Find and setup subviews
_listView = (ListView)_rootView.findViewById(R.id.listView);
...
} else {
// Do not inflate the layout again.
// The returned View of onCreateView will be added into the fragment.
// However it is not allowed to be added twice even if the parent is same.
// So we must remove _rootView from the existing parent view group
// in onDestroyView() (it will be added back).
}
return _rootView;
}
@Override
public void onDestroyView() {
if (_rootView.getParent() != null) {
((ViewGroup)_rootView.getParent()).removeView(_rootView);
}
super.onDestroyView();
}
}
¡¡¡ADVERTENCIA!!!
Este es un HACK! Aunque lo estoy usando en mi aplicación, debe probar y leer los comentarios con cuidado.
Encontré este problema en un Fragmento que contiene un mapa, que tiene demasiados detalles de configuración para guardar / recargar. Mi solución fue básicamente mantener este Fragmento activo todo el tiempo (similar a lo que @kaushal mencionó).
Digamos que tiene el Fragmento A actual y quiere mostrar el Fragmento B. Resumiendo las consecuencias:
- replace () - elimina el Fragmento A y sustitúyelo por el Fragmento B. El Fragmento A se volverá a crear una vez que se vuelva a colocar en el frente.
- add () - (crear y) agregar un Fragmento B y superponer el Fragmento A, que aún está activo en el fondo
- remove () - se puede usar para eliminar el Fragmento B y volver a A. El Fragmento B se volverá a crear cuando se llame más adelante.
Por lo tanto, si desea mantener ambos fragmentos "guardados", simplemente alternarlos usando hide () / show ().
Pros : método fácil y simple para mantener múltiples Fragmentos corriendo
Contras : usa mucha más memoria para mantener todos funcionando. Puede tener problemas, por ejemplo, mostrar muchos mapas de bits de gran tamaño
Mi problema fue similar, pero me superé sin mantener el fragmento vivo. Supongamos que tiene una actividad que tiene 2 fragmentos: F1 y F2. F1 se inicia inicialmente y digamos que contiene cierta información de usuario y luego, con alguna condición, F2 aparece al pedirle al usuario que complete un atributo adicional : su número de teléfono. A continuación, quiere que ese número de teléfono vuelva a F1 y complete el registro, pero se da cuenta de que se perdió toda la información del usuario anterior y no tiene sus datos anteriores. El fragmento se recrea desde cero e incluso si guardó esta información en onSaveInstanceState
el paquete vuelve a cero en onActivityCreated
.
Solución: guarde la información requerida como una variable de instancia en la actividad de llamadas. Luego pasa esa variable de instancia a tu fragmento.
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Bundle args = getArguments();
// this will be null the first time F1 is created.
// it will be populated once you replace fragment and provide bundle data
if (args != null) {
if (args.get("your_info") != null) {
// do what you want with restored information
}
}
}
Entonces, siguiendo con mi ejemplo: antes de mostrar F2, guardo datos de usuario en la variable de instancia usando una devolución de llamada. Luego, inicio F2, el usuario completa el número de teléfono y presiona guardar. Utilizo otra devolución de llamada en actividad, recopilo esta información y reemplazo mi fragmento F1, esta vez tiene datos agrupados que puedo usar.
@Override
public void onPhoneAdded(String phone) {
//replace fragment
F1 f1 = new F1 ();
Bundle args = new Bundle();
yourInfo.setPhone(phone);
args.putSerializable("you_info", yourInfo);
f1.setArguments(args);
getFragmentManager().beginTransaction()
.replace(R.id.fragmentContainer, f1).addToBackStack(null).commit();
}
}
Puede encontrar más información sobre las devoluciones de llamada aquí: https://developer.android.com/training/basics/fragments/communicating.html
Reemplace un Fragmento usando el siguiente código:
Fragment fragment = new AddPaymentFragment();
getSupportFragmentManager().beginTransaction().replace(R.id.frame, fragment, "Tag_AddPayment")
.addToBackStack("Tag_AddPayment")
.commit();
La actividad onBackPressed () es:
@Override
public void onBackPressed() {
android.support.v4.app.FragmentManager fm = getSupportFragmentManager();
if (fm.getBackStackEntryCount() > 1) {
fm.popBackStack();
} else {
finish();
}
Log.e("popping BACKSTRACK===> ",""+fm.getBackStackEntryCount());
}
Si regresa a un fragmento de la pila posterior, no vuelve a crear el fragmento pero vuelve a utilizar la misma instancia y comienza con onCreateView()
en el ciclo de vida del fragmento, consulte Fragmento del ciclo de vida .
Por lo tanto, si desea almacenar el estado, debe usar variables de instancia y no confiar en onSaveInstanceState()
.
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);
Supongo que hay una forma alternativa de lograr lo que estás buscando. No digo que sea una solución completa, pero cumplió el propósito en mi caso.
Lo que hice fue reemplazar el fragmento en el fragmento de objetivo que acababa de agregar. Entonces, básicamente, usarás el método add()
lugar de replace()
.
¿Qué más hice? Escondo mi fragmento actual y también lo agrego al backstack.
Por lo tanto, superpone un nuevo fragmento sobre el fragmento actual sin destruir su vista (compruebe que su método onDestroyView()
no se está llamando. Además, agregarlo a backstate
me da la ventaja de reanudar el fragmento.
Aquí está el código:
Fragment fragment=new DestinationFragment();
FragmentManager fragmentManager = getFragmentManager();
android.app.FragmentTransaction ft=fragmentManager.beginTransaction();
ft.add(R.id.content_frame, fragment);
ft.hide(SourceFragment.this);
ft.addToBackStack(SourceFragment.class.getName());
ft.commit();
El sistema AFAIK solo llama onCreateView()
si la vista se destruye o no se crea. Pero aquí hemos guardado la vista al no eliminarla de la memoria. Por lo tanto, no creará una nueva vista.
Y cuando regrese de Destination Fragment, aparecerá el último FragmentTransaction
eliminará el fragmento superior que hará que la vista superior (SourceFragment) aparezca sobre la pantalla.
COMENTARIO: Como he dicho, no es una solución completa, ya que no elimina la vista del fragmento Fuente y, por lo tanto, ocupa más memoria de la habitual. Pero aún sirve al propósito. También estamos utilizando un mecanismo totalmente diferente para ocultar la vista en lugar de reemplazar lo que no es tradicional
Por lo tanto, no se trata de cómo se mantiene el estado, sino de cómo se mantiene la vista.
onSaveInstanceState()
solo se onSaveInstanceState()
si hay un cambio de configuración.
Desde el cambio de un fragmento a otro no hay cambio de configuración, por lo que no hay ninguna llamada a onSaveInstanceState()
allí. ¿Qué estado no está siendo guardado? ¿Puedes especificar?
Si ingresas texto en EditText, se guardará automáticamente. Cualquier elemento de UI sin ID es el elemento cuyo estado de vista no se guardará.
Public void replaceFragment(Fragment mFragment, int id, String tag, boolean addToStack) {
FragmentTransaction mTransaction = getSupportFragmentManager().beginTransaction();
mTransaction.replace(id, mFragment);
hideKeyboard();
if (addToStack) {
mTransaction.addToBackStack(tag);
}
mTransaction.commitAllowingStateLoss();
}
replaceFragment(new Splash_Fragment(), R.id.container, null, false);
getSupportFragmentManager().addOnBackStackChangedListener(new FragmentManager.OnBackStackChangedListener()
{
@Override
public void onBackStackChanged()
{
if (getSupportFragmentManager().getBackStackEntryCount() == 0)
{
//setToolbarTitle("Main Activity");
}
else
{
Log.e("fragment_replace11111", "replace");
}
}
});
YourActivity.java
@Override
public void onBackPressed()
{
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.Fragment_content);
if (fragment instanceof YourFragmentName)
{
fragmentReplace(new HomeFragment(),"Home Fragment");
txt_toolbar_title.setText("Your Fragment");
}
else{
super.onBackPressed();
}
}
public void fragmentReplace(Fragment fragment, String fragment_name)
{
try
{
fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.Fragment_content, fragment, fragment_name);
fragmentTransaction.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right);
fragmentTransaction.addToBackStack(fragment_name);
fragmentTransaction.commitAllowingStateLoss();
}
catch (Exception e)
{
e.printStackTrace();
}
}