Cycle de vie du fragment Android sur les changements d'orientation

120

Utilisation du package de compatibilité pour cibler 2.2 à l'aide de fragments.

Après avoir recodé une activité pour utiliser des fragments dans une application, je n'ai pas pu faire fonctionner les changements d'orientation / gestion de l'état, j'ai donc créé une petite application de test avec un seul FragmentActivity et un seul Fragment.

Les journaux des changements d'orientation sont étranges, avec plusieurs appels aux fragments OnCreateView.

Il me manque évidemment quelque chose - comme détacher le fragment et le rattacher plutôt que de créer une nouvelle instance, mais je ne vois aucune documentation qui indiquerait où je vais mal.

Quelqu'un peut-il faire la lumière sur ce que je fais de mal ici, s'il vous plaît. Merci

Le journal est le suivant après les changements d'orientation.

Initial creation
12-04 11:57:15.808: D/FragmentTest.FragmentTestActivity(3143): onCreate
12-04 11:57:15.945: D/FragmentTest.FragmentOne(3143): OnCreateView
12-04 11:57:16.081: D/FragmentTest.FragmentOne(3143): OnCreateView->SavedInstanceState null


Orientation Change 1
12-04 11:57:39.031: D/FragmentTest.FragmentOne(3143): onSaveInstanceState
12-04 11:57:39.031: D/FragmentTest.FragmentTestActivity(3143): onCreate
12-04 11:57:39.031: D/FragmentTest.FragmentOne(3143): OnCreateView
12-04 11:57:39.031: D/FragmentTest.FragmentOne(3143): OnCreateView->SavedInstanceState not null
12-04 11:57:39.031: D/FragmentTest.FragmentOne(3143): OnCreateView
12-04 11:57:39.167: D/FragmentTest.FragmentOne(3143): OnCreateView->SavedInstanceState null


Orientation Change 2
12-04 11:58:32.162: D/FragmentTest.FragmentOne(3143): onSaveInstanceState
12-04 11:58:32.162: D/FragmentTest.FragmentOne(3143): onSaveInstanceState
12-04 11:58:32.361: D/FragmentTest.FragmentTestActivity(3143): onCreate
12-04 11:58:32.361: D/FragmentTest.FragmentOne(3143): OnCreateView
12-04 11:58:32.361: D/FragmentTest.FragmentOne(3143): OnCreateView->SavedInstanceState not null
12-04 11:58:32.361: D/FragmentTest.FragmentOne(3143): OnCreateView
12-04 11:58:32.361: D/FragmentTest.FragmentOne(3143): OnCreateView->SavedInstanceState not null
12-04 11:58:32.498: D/FragmentTest.FragmentOne(3143): OnCreateView
12-04 11:58:32.498: D/FragmentTest.FragmentOne(3143): OnCreateView->SavedInstanceState null

Activité principale (FragmentActivity)

public class FragmentTestActivity extends FragmentActivity {
/** Called when the activity is first created. */

private static final String TAG = "FragmentTest.FragmentTestActivity";


FragmentManager mFragmentManager;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Log.d(TAG, "onCreate");

    mFragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

    FragmentOne fragment = new FragmentOne();

    fragmentTransaction.add(R.id.fragment_container, fragment);
    fragmentTransaction.commit();
}

Et le fragment

public class FragmentOne extends Fragment {

private static final String TAG = "FragmentTest.FragmentOne";

EditText mEditText;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {

    Log.d(TAG, "OnCreateView");

    View v = inflater.inflate(R.layout.fragmentonelayout, container, false);

    // Retrieve the text editor, and restore the last saved state if needed.
    mEditText = (EditText)v.findViewById(R.id.editText1);

    if (savedInstanceState != null) {

        Log.d(TAG, "OnCreateView->SavedInstanceState not null");

        mEditText.setText(savedInstanceState.getCharSequence("text"));
    }
    else {
        Log.d(TAG,"OnCreateView->SavedInstanceState null");
    }
    return v;
}

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    Log.d(TAG, "FragmentOne.onSaveInstanceState");

    // Remember the current text, to restore if we later restart.
    outState.putCharSequence("text", mEditText.getText());
}

Manifeste

<uses-sdk android:minSdkVersion="8" />

<application
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name" >
    <activity
        android:label="@string/app_name"
        android:name=".activities.FragmentTestActivity" 
        android:configChanges="orientation">
        <intent-filter >
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>
MartinS
la source
Je ne sais pas si c'est une bonne réponse, mais essayez d'utiliser une balise lors de l'ajout du fragment, ajoutez (R.id.fragment_container, fragment, "MYTAG"), ou à défaut, remplacez (R.id.fragment_container, fragment, "MYTAG ")
Jason
2
Faire quelques investigations. Lorsque l'activité principale (FragmentTestActivity) redémarre lors du changement d'orientation et que j'obtiens une nouvelle instance de FragmentManager, j'effectue un FindFragmentByTag pour localiser le fragment qui existe toujours, de sorte que le fragment est conservé pendant la recréation de l'activité principale. Si je trouve le fragment et que je ne fais rien, il est de toute façon réaffiche avec MainActivity.
MartinS

Réponses:

189

Vous superposez vos fragments les uns sur les autres.

Lorsqu'un changement de configuration se produit, l'ancien fragment s'ajoute à la nouvelle activité lorsqu'il est recréé. C'est une douleur énorme à l'arrière la plupart du temps.

Vous pouvez empêcher les erreurs de se produire en utilisant le même fragment au lieu d'en recréer un nouveau. Ajoutez simplement ce code:

if (savedInstanceState == null) {
    // only create fragment if activity is started for the first time
    mFragmentManager = getSupportFragmentManager();
    FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();

    FragmentOne fragment = new FragmentOne();

    fragmentTransaction.add(R.id.fragment_container, fragment);
    fragmentTransaction.commit();
} else {        
    // do nothing - fragment is recreated automatically
}

Soyez averti cependant: des problèmes se produiront si vous essayez d'accéder aux vues d'activité depuis l'intérieur du fragment car les cycles de vie changeront subtilement. (Obtenir des vues d'une activité parent à partir d'un fragment n'est pas facile).

Graeme
la source
54
"C'est une douleur énorme à l'arrière la plupart du temps" (pouces vers le haut)
rushinge
1
Comment gérer le même scénario en cas d'utilisation de ViewPage avec FragmentStatePagerAdapter ... une suggestion?
CoDe
5
Y a-t-il une affirmation similaire dans la documentation officielle? N'est-ce pas une contradiction avec ce qui est indiqué dans le guide "when the activity is destroyed, so are all fragments":? Depuis "When the screen orientation changes, the system destroys and recreates the activity [...]".
cYrus
4
Cyrus - Non, l'activité est effectivement détruite, les fragments qu'elle contient sont référencés dans le FragmentManager, pas uniquement à partir de l'activité, donc il reste et est lu.
Graeme
4
la journalisation des fragments des méthodes onCreate et onDestroy ainsi que son hashcode après avoir trouvé dans FragmentManager montre clairement que le fragment EST détruit. il est simplement recréé et rattaché automatiquement. seulement si vous mettez setRetainInstance (true) dans la méthode fragments onCreate, il ne sera vraiment pas détruit
Lemao1981
87

Pour citer ce livre , "pour garantir une expérience utilisateur cohérente, Android conserve la mise en page Fragment et la pile arrière associée lorsqu'une activité est redémarrée en raison d'un changement de configuration." (p. 124)

Et la façon d'aborder cela consiste à vérifier d'abord si la pile arrière de fragments a déjà été remplie et à créer la nouvelle instance de fragment uniquement si ce n'est pas le cas:

@Override
public void onCreate(Bundle savedInstanceState) {

        ...    

    FragmentOne fragment = (FragmentOne) mFragmentManager.findFragmentById(R.id.fragment_container); 

    if (fragment == null) {
        FragmentTransaction fragmentTransaction = mFragmentManager.beginTransaction();
        fragmentTransaction.add(R.id.fragment_container, new FragmentOne());
        fragmentTransaction.commit();
    }
}
k29
la source
2
Vous m'avez probablement fait gagner beaucoup de temps avec celui-ci ... merci beaucoup. Vous pouvez combiner cette réponse avec celle de Graeme pour obtenir une solution parfaite pour gérer les changements de configuration et les fragments.
azpublic
10
C'est en fait la bonne réponse, pas celle marquée. Merci beaucoup!
Uriel Frankel
comment peut gérer le même scénario en cas d'implémentation de ViewPager Fragment.
CoDe
Ce petit bijou a aidé sur un problème que je cherchais depuis plusieurs jours. Je vous remercie! C'est définitivement la solution.
Whome
1
@SharpEdge Si vous avez plusieurs fragments, vous devez leur donner des balises lors de l'ajout au conteneur, puis utiliser mFragmentManager.findFragmentByTag (au lieu de findFragmentById) pour obtenir des références à eux - de cette façon vous connaîtrez la classe de chaque fragment et pourrez cast correctement
k29
10

La méthode onCreate () de votre activité est appelée après le changement d'orientation comme vous l'avez vu. Par conséquent, n'exécutez pas la FragmentTransaction qui ajoute le fragment après le changement d'orientation dans votre activité.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState==null) {
        //do your stuff
    }
}

Les fragments doivent et doivent rester inchangés.

Αλέκος
la source
Savons-nous que l'instance sera enregistrée après la création et l'ajout du fragment? Je veux dire chapeau si un utilisateur tourne juste avant que le fragment ne soit ajouté? Nous aurons toujours un SavedInstanceState non nul qui ne contient pas l'état du fragment
Farid
4

Vous pouvez @Overrideutiliser FragmentActivity onSaveInstanceState(). Veillez à ne pas appeler le super.onSaveInstanceState()dans la méthode.

Victor.Chan
la source
2
Cela briserait très probablement le cycle de vie des activités, introduisant plus de problèmes potentiels dans ce processus déjà assez compliqué. Regardez dans le code source de FragmentActivity: il enregistre les états de tous les fragments là-bas.
Brian
J'ai eu le problème que j'ai un nombre d'adaptateurs différent pour une orientation différente. J'ai donc toujours eu une situation étrange après avoir tourné l'appareil et glissé quelques pages, j'ai eu l'ancienne et la mauvaise. avec la rotation de l'instance enregistrée, cela fonctionne mieux sans fuites de mémoire (j'ai utilisé setSavedEnabled (false) avant et j'ai fini avec de grandes fuites de mémoire à chaque changement d'orientation)
Informatic0re
0

Nous devrions toujours essayer d'éviter les exceptions nullpointer, nous devons donc d'abord vérifier dans la méthode saveinstance les informations sur le bundle. pour une brève explication pour vérifier ce lien de blog

public static class DetailsActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (getResources().getConfiguration().orientation
            == Configuration.ORIENTATION_LANDSCAPE) {
            // If the screen is now in landscape mode, we can show the
            // dialog in-line with the list so we don't need this activity.
            finish();
            return;
        }

        if (savedInstanceState == null) {
            // During initial setup, plug in the details fragment.
            DetailsFragment details = new DetailsFragment();
            details.setArguments(getIntent().getExtras());
            getFragmentManager().beginTransaction().add(android.R.id.content, details).commit();
        }
    } 
}
abhi
la source
0

Si vous ne faites qu'un projet, le chef de projet dit que vous devez obtenir un écran de fonction de commutation, mais que vous ne voulez pas afficher une mise en page différente de charge de commutation (peut créer un système de mise en page et de port de mise en page.

Vous allez déterminer automatiquement l'état de l'écran, charger la mise en page correspondante), en raison de la nécessité de réinitialiser l'activité ou le fragment, l'expérience utilisateur n'est pas bonne, pas directement sur le commutateur d'écran, je me réfère? Url = YgNfP-vHy-Nuldi7YHTfNet3AtLdN-w__O3z1wLOnzr3wDjYo7X7PYdNyhw8R24ZE22xiKnydni7R0r35s2fOLcHOiLGYT9Qh_fjqtytJkie & wd1087008000 = fj0000800001

Le principe est que votre mise en page utilise le poids de la façon dont la mise en page de layout_weight, comme suit:

<LinearLayout
Android:id= "@+id/toplayout"
Android:layout_width= "match_parent"
Android:layout_height= "match_parent"
Android:layout_weight= "2"
Android:orientation= "horizontal" >

Mon approche est donc, lors du changement d'écran, de ne pas avoir besoin de charger une nouvelle mise en page du fichier de vue, de modifier la mise en page dans les pondérations dynamiques onConfigurationChanged, les étapes suivantes: 1 premier jeu: AndroidManifest.xml dans l'attribut d'activité: android: configChanges = "keyboardHidden | orientation | screenSize" Pour éviter le changement d'écran, évitez le rechargement, afin de pouvoir surveiller dans onConfigurationChanged 2 l'activité de réécriture ou le fragment dans la méthode onConfigurationChanged.

@Override
Public void onConfigurationChanged (Configuration newConfig) {
    Super.onConfigurationChanged (newConfig);
    SetContentView (R.layout.activity_main);
    if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
        //On the layout / / weight adjustment
        LinearLayout toplayout = (LinearLayout) findViewById (R.id.toplayout);
        LinearLayout.LayoutParams LP = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 2.0f);
        Toplayout.setLayoutParams (LP);
        LinearLayout tradespace_layout = (LinearLayout) findViewById(R.id.tradespace_layout);
        LinearLayout.LayoutParams LP3 = new LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 2.8f);
        Tradespace_layout.setLayoutParams (LP3);
    }
    else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT)
    {
        //On the layout / / weight adjustment
        LinearLayout toplayout = (LinearLayout) findViewById (R.id.toplayout);
        LinearLayout.LayoutParams LP = new LayoutParams (LinearLayout.LayoutParams.MATCH_PARENT, 0, 2.8f);
        Toplayout.setLayoutParams (LP);
        LinearLayout tradespace_layout = (LinearLayout) findViewById (R.id.tradespace_layout);
        LinearLayout.LayoutParams LP3 = new LayoutParams (LinearLayout.LayoutParams.MATCH_PARENT, 0, 2.0f);
        Tradespace_layout.setLayoutParams (LP3);
    }
}
nihaoqiulinhe
la source
0

Lors du changement de configuration, le framework créera une nouvelle instance du fragment pour vous et l'ajoutera à l'activité. Donc au lieu de ça:

FragmentOne fragment = new FragmentOne();

fragmentTransaction.add(R.id.fragment_container, fragment);

faites ceci:

if (mFragmentManager.findFragmentByTag(FRAG1_TAG) == null) {
    FragmentOne fragment = new FragmentOne();

    fragmentTransaction.add(R.id.fragment_container, fragment, FRAG1_TAG);
}

Veuillez noter que le framework ajoute une nouvelle instance de FragmentOne lors d'un changement d'orientation, sauf si vous appelez setRetainInstance (true), auquel cas il ajoutera l'ancienne instance de FragmentOne.

vlazzle
la source