Conservation des données temporaires
-
Destruction possible d'une activité :
- En cas de changement de configuration de l'application (cas du changement de géométrie avec la rotation de l'écran)
- En cas de pénurie de mémoire centrale
-
Avant destruction, sauvegarde manuelle des structures de données temporaires utiles → restauration de l'état de l'activité lors de la réinstantiation
-
Sauvegarde temporaire sur disque sans persistance au redémarrage du système
-
onSaveInstanceState(Bundle outState) : sauvegarde des données dans le Bundle
- Redéfinition nécessaire pour sauvegarder les données de l'activité, ne pas oublier d'appeler la méthode super pour sauvegarder le contenu des EditText en cours d'édition
- Données devant être sérialisables
- Ne pas sauvegarder des données trop volumineuses (coût de la sérialisation)
- onCreate(Bundle state) : restauration des données (bien vérifier que le Bundle n'est pas nul)
-
onSaveInstanceState(Bundle outState) : sauvegarde des données dans le Bundle
-
Sauvegarde en mémoire centrale (références accessibles depuis un singleton par exemple)
- Aucun coût temporel (conservation de références), pas de sérialisation
- Coût en mémoire centrale (objets non-récupérables par le ramasse-miette)
- Fonctionne en cas de changement de configuration
- Ne fonctionne pas en cas de pénurie de mémoire centrale (le processus est détruit ainsi que ses données en tas)
-
Sauvegarde temporaire sur disque sans persistance au redémarrage du système
Exemple : une activité pour réaliser une conservation d'état
Ecriture de l'activité ConfigChangeSupportActivity réalisant une sauvegarde dans une Map statique d'un objet d'état contenant toutes les données de l'activité avec implantation du code de restauration dans onCreate(Bundlbe b) et de sauvegarde dans onSaveInstanceState(Bundle b).
// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License package fr.upem.coursand.configchange; import android.os.Bundle; import android.os.SystemClock; import androidx.appcompat.app.AppCompatActivity; import android.util.Log; import android.util.Pair; import java.lang.ref.WeakReference; import java.lang.reflect.Field; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; /** This class can be used as an ancestor for classes that need to support configuration changes. * By default when there is a configuration change, the activity is destroyed then recreated. * In this case all the date stored into the fields of the old class instance is lost * for the new class instance. * * This ancestor class uses the followin approach to store and restore data from the ancien activity: * - We assume that there is only one field storing state data for the activity; this field is named 'state' * (typically one create an inner static class State implementing containing all the required data) * - Before the destruction of the activity we save the reference of state in a map * - Then we reinitialize the 'state' field with this new stored value */ public class ConfigChangeSupportActivity extends AppCompatActivity { public static final String STATE_FIELD_NAME = "state"; public static final String ACTIVITY_ID_KEY = ConfigChangeSupportActivity.class.getName() + ".activityID"; /** Maximal lifetime of a state entry (time between destruction and recreation of the activity) */ public static final int DEFAULT_STATE_LIFETIME = 600; // in seconds /** A map saving the states for all the activities that are going to be destroyed and recreated * The value is a pair: the first element is the state objet and the second the date when the hosting * was about to be destroyed (using SystemClock.elapsedTime() */ private static final Map<String, Pair<Object, Long>> statesMap = new HashMap<>(); /** Current instance of activity for each identifier. * Using a WekReference is an extra precaution that is not compulsory * if we remove the value from the map on onDestroy(). * */ private static final Map<String, WeakReference<ConfigChangeSupportActivity>> activityInstances = new HashMap<>(); /** Persistent identifier of the activity (should be saved across activity recreations) */ private String activityID = null; /* Get the current activity instance for the given identifier */ protected ConfigChangeSupportActivity getCurrentActivityInstance() { return activityInstances.get(activityID).get(); } /** Find the 'state' field using introspection */ protected Field findStateField() { try { Field f = getClass().getDeclaredField(STATE_FIELD_NAME); f.setAccessible(true); // to render the field accessible return f; } catch (NoSuchFieldException e) { return null; } } /** Purge the statesMap by removing all the expired states (inserted more than x second ago) */ private void purgeStatesMap() { Set<String> identifiersToBeRemoved = new HashSet<>(); for (Map.Entry<String, Pair<Object, Long>> entry: statesMap.entrySet()) if (entry.getValue().second < SystemClock.elapsedRealtime()) identifiersToBeRemoved.add(entry.getKey()); statesMap.keySet().removeAll(identifiersToBeRemoved); } /** Return the lifetime of the state in seconds (can be overridden) */ protected int getStateLifetime() { return DEFAULT_STATE_LIFETIME; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); activityID = savedInstanceState != null ? savedInstanceState.getString(ACTIVITY_ID_KEY) : null; if (activityID != null) { Pair<Object, Long> stateAndDate = statesMap.get(activityID); if (stateAndDate != null) try { findStateField().set(this, stateAndDate.first); statesMap.remove(activityID); } catch (IllegalAccessException e) { throw new RuntimeException(e); // improbable } else Log.w(getClass().getName(), "The state of the activity " + getClass().getName() + " of identifier " + activityID + " is not present"); } else activityID = UUID.randomUUID().toString(); // choose a random value for the activityID // update the activity instances map activityInstances.put(activityID, new WeakReference<>(this)); // purge the statesMap by removing all the expired states (inserted more than x second ago) purgeStatesMap(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(ACTIVITY_ID_KEY, activityID); Object state = null; try { Field f = findStateField(); if (f != null) state = f.get(this); } catch (IllegalAccessException e) { throw new RuntimeException(e); } if (state != null) statesMap.put(activityID, new Pair<>(state, SystemClock.elapsedRealtime() + getStateLifetime() * 1000L)); } @Override protected void onDestroy() { super.onDestroy(); activityInstances.remove(activityID); } }
Gestion du changement de configuration
- Comportement par défaut en cas de changement de configuration : destruction et recréation d'activité
- Possibilite de demander de ne pas détruire/recréer l'activité dans le manifeste avec par exemple (pour gérer le changement d'orientation, l'affichage du clavier virtuel et le changement de taill d'écran) :
<activity android:name=".MyActivity" android:configChanges="orientation|keyboard|keyboardHidden|screenSize" android:label="@string/app_name">
- Ecriture possible d'une méthode pour réaliser des actions lors d'un changement de configuration prévu dans android:configChanges :
@Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); // Checks the orientation of the screen if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) { Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show(); } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){ Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show(); } }
- Nécessité malgré l'usage de android:configChanges de gérer les cas de destruction/recréation d'activité pouvant survenir en cas de pénurie système
- Gestion déconseillée avec android:configChanges des changements de locale, de fontScale (préférable de détruire/recréer l'activité plutôt que d'écrire le code de prise en charge dans onConfigurationChanged()