web-dev-qa-db-fra.com

Comment puis-je conserver l'état de fragment une fois ajouté à la pile arrière?

J'ai écrit une activité fictive qui alterne entre deux fragments. Lorsque vous passez de FragmentA à FragmentB, FragmentA est ajouté à la pile arrière. Cependant, lorsque je retourne à FragmentA (en appuyant dessus), un tout nouveau FragmentA est créé et l’état dans lequel il se trouvait est perdu. J'ai l'impression que je suis après la même chose que this question, mais j'ai inclus un exemple de code complet pour aider à résoudre le problème:

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);
    }
  }
}

Avec quelques messages de journal ajoutés:

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

Il n'appelle jamais FragmentA.onSaveInstanceState et crée un nouveau FragmentA lorsque vous répondez. Cependant, si je suis sur FragmentA et que je verrouille l'écran, FragmentA.onSaveInstanceState est appelé. Tellement bizarre ... ai-je tort de m'attendre à ce qu'un fragment ajouté à la pile arrière n'ait pas besoin d'être recréé? Voici ce que disent les docs :

Par contre, si vous appelez addToBackStack () lors de la suppression d'un fragment, celui-ci est arrêté et sera repris si l'utilisateur revient en arrière.

147
Eric

Si vous revenez à un fragment de la pile arrière, il ne recrée pas le fragment, mais réutilise la même instance et commence par onCreateView() dans le cycle de vie du fragment, voir cycle de vie du fragment .

Donc, si vous voulez stocker l’état, vous devez utiliser des variables d’instance et pas ne dépendez pas de onSaveInstanceState().

114
Jan-Henk

Comparé à UINavigationController et UIViewController d’Apple, Google ne fonctionne pas bien dans l’architecture logicielle Android. Et le document d'Android sur Fragment n'aide pas beaucoup.

Lorsque vous entrez FragmentB à partir de FragmentA, l'instance FragmentA existante n'est pas détruite. Lorsque vous appuyez sur Retour dans FragmentB et revenez à FragmentA, nous ne créons pas de nouvelle instance FragmentA. La onCreateView() de l'instance FragmentA existante sera appelée.

L'essentiel est de ne pas gonfler à nouveau la vue dans onCreateView() de FragmentA, car nous utilisons l'instance existante de FragmentA. Nous devons enregistrer et réutiliser le rootView.

Le code suivant fonctionne bien. Il ne conserve pas seulement l'état de fragment, il réduit également RAM et la charge du processeur (car nous ne faisons que gonfler la présentation si nécessaire). Je ne peux pas croire que l'exemple de code et le document de Google ne le mentionnent jamais mais toujours gonfler la présentation .

Version 1 (N'utilisez pas la version 1. Utilisez la version 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;
    }
}

------ Mise à jour du 3 mai 2005: -------

Comme mentionné dans les commentaires, parfois _rootView.getParent() est nul dans onCreateView, ce qui provoque le blocage. La version 2 supprime _rootView dans onDestroyView (), comme suggéré par Dell116. Testé sur Android 4.0.3, 4.4.4, 5.1.0.

Version 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();
    }
}

ATTENTION !!!

C'est un HACK! Bien que je l'utilise dans mon application, vous devez tester et lire les commentaires attentivement.

79
Vince Yuan

Je suppose qu’il existe un autre moyen de réaliser ce que vous recherchez. Je ne dis pas que c'est une solution complète, mais elle a servi à cela dans mon cas.

Au lieu de remplacer le fragment, j'ai simplement ajouté un fragment cible. Donc, fondamentalement, vous allez utiliser la méthode add() à la place de replace().

Quoi d'autre j'ai fait. Je cache mon fragment actuel et l'ajoute également à backstack.

Par conséquent, il chevauche un nouveau fragment sur le fragment actuel sans détruire sa vue (vérifiez que sa méthode onDestroyView() n'est pas appelée. De plus, l'ajouter à backstate me donne l'avantage de reprendre le fragment.

Voici le code:

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();

Le système AFAIK appelle uniquement onCreateView() si la vue est détruite ou non créée. Mais ici nous avons sauvegardé la vue en ne la supprimant pas de la mémoire. Donc, cela ne créera pas de nouvelle vue.

Et lorsque vous revenez de Fragment de destination, le dernier FragmentTransaction apparaît, ce qui supprime le fragment supérieur qui permet à la vue la plus haute (SourceFragment) d'apparaître à l'écran.

COMMENTAIRE: Comme je l'ai dit, ce n'est pas une solution complète, car elle ne supprime pas la vue du fragment source et occupe donc plus de mémoire que d'habitude. Mais toujours, servir le but. De plus, nous utilisons un mécanisme totalement différent de masquage de vue au lieu de le remplacer, ce qui n’est pas traditionnel.

Donc, ce n'est pas vraiment pour la manière dont vous maintenez l'état, mais pour celle pour laquelle vous maintenez la vue.

49
kaushal trivedi

Je suis tombé sur ce problème dans un fragment contenant une carte, qui contient trop de détails de configuration pour pouvoir enregistrer/recharger. Ma solution était de garder ce fragment actif tout le temps (comme ce que @kaushal a mentionné).

Supposons que vous avez le fragment A actuel et que vous souhaitez afficher le fragment B. Résumant les conséquences:

  • replace () - supprime le fragment A et le remplace par le fragment B. Le fragment A sera recréé une fois ramené à l'avant
  • add () crée et) ajoute un fragment B qui chevauche le fragment A, qui est toujours actif en arrière-plan
  • remove () - peut être utilisé pour supprimer le fragment B et retourner à A. Le fragment B sera recréé lors de l'appel ultérieur

Par conséquent, si vous souhaitez conserver les deux fragments "sauvegardés", basculez-les simplement en utilisant hide ()/show ().

--- (Pros: méthode simple et facile pour garder plusieurs fragments en cours d'exécution
Cons: vous utilisez beaucoup plus de mémoire pour les maintenir en fonctionnement. Peut rencontrer des problèmes, par exemple affichage de nombreuses grandes bitmaps

6
Teo Inke

onSaveInstanceState() n'est appelé que s'il y a un changement de configuration.

Depuis le passage d'un fragment à un autre, il n'y a pas de changement de configuration, donc aucun appel à onSaveInstanceState() n'est présent. Quel état n'est pas en cours de sauvegarde? Pouvez-vous préciser?

Si vous entrez du texte dans EditText, il sera automatiquement enregistré. Tout élément d'interface utilisateur sans ID est l'élément dont l'état d'affichage ne doit pas être enregistré.

3
user2779311

Ici, puisque onSaveInstanceState in fragment n'appelle pas lorsque vous ajoutez un fragment à backstack. Le cycle de vie des fragments dans le backstack lorsqu'il est restauré commence onCreateView et se termine onDestroyView alors que onSaveInstanceState est appelé entre onDestroyView et onDestroy. Ma solution est de créer une variable d'instance et init dans onCreate. Exemple de code:

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();
}

Et vérifiez dans 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
}
0
Ling Boo

first: utilisez simplement la méthode add au lieu de la méthode replace de la classe FragmentTransaction, vous devez ensuite ajouter secondFragment à empiler avec la méthode addToBackStack

second: au dernier clic, vous devez appeler 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();                                
        }
};
0
MiladAhmadi
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();
    }
}
0
Hardik Vasani

Mon problème était similaire mais je me suis surmonté sans garder le fragment en vie. Supposons que vous ayez une activité comportant 2 fragments - F1 et F2. F1 est démarré initialement et disons que contient des informations sur l’utilisateur, puis sur une condition, F2 apparaît pour demander à l’utilisateur de renseigner un attribut supplémentaire - leur numéro de téléphone. Ensuite, vous souhaitez que ce numéro de téléphone réapparaisse en F1 et que l'inscription soit complète, mais vous réalisez que toutes les informations des utilisateurs précédents sont perdues et que vous ne disposez pas de leurs données précédentes. Le fragment est recréé à partir de zéro et même si vous avez sauvegardé ces informations dans onSaveInstanceState, le paquet renvoie null dans onActivityCreated.

Solution: Enregistrez les informations requises en tant que variable d'instance dans l'activité d'appel. Puis passez cette variable d'instance dans votre fragment.

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

Voici donc mon exemple: avant d’afficher F2, j’enregistre les données utilisateur dans la variable d’instance à l’aide d’un rappel. Ensuite, je lance F2, l’utilisateur entre le numéro de téléphone et appuie sur enregistrer. J'utilise un autre rappel en activité, recueille ces informations et remplace mon fragment F1; cette fois, il contient des données d'ensemble que je peux utiliser.

@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();

    }
}

Plus d'informations sur les rappels peuvent être trouvées ici: https://developer.Android.com/training/basics/fragments/communicating.html

0
Dodi
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);
0
Hitesh Manwani

Remplacez un fragment en utilisant le code suivant:

Fragment fragment = new AddPaymentFragment();
getSupportFragmentManager().beginTransaction().replace(R.id.frame, fragment, "Tag_AddPayment")
                .addToBackStack("Tag_AddPayment")
                .commit();

L'activité onBackPressed () est:

  @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());

}
0
Pratibha Sarode

Solution parfaite qui trouve un vieux fragment dans la pile et le charge s'il existe dans la pile.

/**
     * 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();
    }

l'utiliser comme

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