Comment enregistrer un état d'activité Android à l'aide de l'état d'instance de sauvegarde?

StackOverflow https://stackoverflow.com/questions/151777

Question

Je travaille sur la plate-forme Android SDK et il est difficile de savoir comment enregistrer l'état d'une application. Donc, compte tenu de cette réorganisation mineure de l'exemple 'Hello, Android':

package com.android.hello;

import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;

public class HelloAndroid extends Activity {

  private TextView mTextView = null;

  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mTextView = new TextView(this);

    if (savedInstanceState == null) {
       mTextView.setText("Welcome to HelloAndroid!");
    } else {
       mTextView.setText("Welcome back.");
    }

    setContentView(mTextView);
  }
}

Je pensais que cela suffirait dans le cas le plus simple, mais il répond toujours avec le premier message, quelle que soit la façon dont je navigue hors de l'application.

Je suis sûr que la solution est aussi simple que de remplacer onPause ou quelque chose du genre, mais j'ai bousculé la documentation pendant environ 30 minutes et je n'ai rien trouvé d'évident.

Était-ce utile?

La solution

Vous devez remplacer onSaveInstanceState (Bundle savedInstanceState) et écrire les valeurs d'état de l'application à modifier pour le paramètre Bundle , comme suit:

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
  super.onSaveInstanceState(savedInstanceState);
  // Save UI state changes to the savedInstanceState.
  // This bundle will be passed to onCreate if the process is
  // killed and restarted.
  savedInstanceState.putBoolean("MyBoolean", true);
  savedInstanceState.putDouble("myDouble", 1.9);
  savedInstanceState.putInt("MyInt", 1);
  savedInstanceState.putString("MyString", "Welcome back to Android");
  // etc.
}

Le paquetage est essentiellement un moyen de stocker une mappe NVP ("paire paire nom-valeur") et elle sera transmise à onCreate () et à onRestoreInstanceState () où vous extrayeriez ensuite les valeurs comme ceci:

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  // Restore UI state from the savedInstanceState.
  // This bundle has also been passed to onCreate.
  boolean myBoolean = savedInstanceState.getBoolean("MyBoolean");
  double myDouble = savedInstanceState.getDouble("myDouble");
  int myInt = savedInstanceState.getInt("MyInt");
  String myString = savedInstanceState.getString("MyString");
}

Vous utiliseriez généralement cette technique pour stocker des valeurs d'instance pour votre application (sélections, texte non enregistré, etc.).

Autres conseils

Le savedInstanceState sert uniquement à enregistrer l'état associé à une instance actuelle d'une activité, par exemple les informations de navigation ou de sélection en cours. Ainsi, si Android détruit et recrée une activité, elle peut revenir telle quelle. était avant. Consultez la documentation de onCreate et onSaveInstanceState

Pour un état plus long, envisagez d'utiliser une base de données SQLite, un fichier ou des préférences. Voir Enregistrement de l'état persistant .

Notez qu'il est NON sûr d'utiliser onSaveInstanceState et onRestoreInstanceState pour les données persistantes , selon la documentation sur les états d'activité dans http://developer.android. com / reference / android / app / Activity.html .

Le document indique (dans la section "Cycle de vie de l'activité"):

  

Notez qu'il est important de sauvegarder   données persistantes dans onPause () à la place   de onSaveInstanceState (Bundle)   parce que ce dernier ne fait pas partie de la   callbacks de cycle de vie, donc ne sera pas   appelé dans chaque situation comme décrit   dans sa documentation.

En d'autres termes, placez votre code de sauvegarde / restauration pour les données persistantes dans onPause () et onResume () !

MODIFIER : pour plus de précisions, voici la documentation sur onSaveInstanceState () :

  

Cette méthode est appelée avant qu'une activité puisse être supprimée, de sorte que   revient dans le futur, il peut restaurer son état. Pour   Par exemple, si l’activité B est lancée devant l’activité A et à certains   l’activité A est tuée pour récupérer des ressources, l’activité A aura   une chance de sauvegarder l’état actuel de son interface utilisateur via cette   méthode de sorte que lorsque l'utilisateur retourne à l'activité A, l'état de   l’interface utilisateur peut être restaurée via onCreate (Bundle) ou    onRestoreInstanceState (Bundle) .

Mon collègue a écrit un article expliquant l'état de l'application sur les appareils Android, y compris des explications sur le cycle de vie d'une activité et des informations d'état, comment stocker des informations d'état et enregistrer dans l'état Bundle et SharedPreferences . et regardez ici .

Cet article aborde trois approches:

Stockez les données de contrôle de la variable locale / interface utilisateur pendant la durée de vie de l'application (temporairement) à l'aide d'un ensemble d'états d'instance

[Code sample – Store state in state bundle]
@Override
public void onSaveInstanceState(Bundle savedInstanceState)
{
  // Store UI state to the savedInstanceState.
  // This bundle will be passed to onCreate on next call.  EditText txtName = (EditText)findViewById(R.id.txtName);
  String strName = txtName.getText().toString();

  EditText txtEmail = (EditText)findViewById(R.id.txtEmail);
  String strEmail = txtEmail.getText().toString();

  CheckBox chkTandC = (CheckBox)findViewById(R.id.chkTandC);
  boolean blnTandC = chkTandC.isChecked();

  savedInstanceState.putString(“Name”, strName);
  savedInstanceState.putString(“Email”, strEmail);
  savedInstanceState.putBoolean(“TandC”, blnTandC);

  super.onSaveInstanceState(savedInstanceState);
}

Stockez les données de contrôle de la variable locale / interface utilisateur entre les instances d'application (c'est-à-dire de manière permanente) à l'aide de préférences partagées

[Code sample – store state in SharedPreferences]
@Override
protected void onPause()
{
  super.onPause();

  // Store values between instances here
  SharedPreferences preferences = getPreferences(MODE_PRIVATE);
  SharedPreferences.Editor editor = preferences.edit();  // Put the values from the UI
  EditText txtName = (EditText)findViewById(R.id.txtName);
  String strName = txtName.getText().toString();

  EditText txtEmail = (EditText)findViewById(R.id.txtEmail);
  String strEmail = txtEmail.getText().toString();

  CheckBox chkTandC = (CheckBox)findViewById(R.id.chkTandC);
  boolean blnTandC = chkTandC.isChecked();

  editor.putString(“Name”, strName); // value to store
  editor.putString(“Email”, strEmail); // value to store
  editor.putBoolean(“TandC”, blnTandC); // value to store
  // Commit to storage
  editor.commit();
}

Conserver les instances d'objet en mémoire entre les activités de la vie de l'application à l'aide d'une instance non configurée conservée

[Code sample – store object instance]
private cMyClassType moInstanceOfAClass; // Store the instance of an object
@Override
public Object onRetainNonConfigurationInstance()
{
  if (moInstanceOfAClass != null) // Check that the object exists
      return(moInstanceOfAClass);
  return super.onRetainNonConfigurationInstance();
}

C’est un classique du développement sur Android. Il y a deux problèmes ici:

  • Il existe un bogue subtil dans Android Framework qui complique grandement la gestion de la pile d'applications pendant le développement, du moins sur les versions héritées (je ne suis pas tout à fait sûr si / quand / comment il a été corrigé). Je vais discuter de ce bug ci-dessous.
  • Le moyen "normal" ou prévu de gérer ce problème est, en soi, assez compliqué avec la dualité onPause / onResume et onSaveInstanceState / onRestoreInstanceState

En parcourant toutes ces discussions, je soupçonne que les développeurs parlent souvent de ces deux problèmes différents en même temps ... d'où toute la confusion et les rapports de "cela ne fonctionne pas pour moi".

Tout d’abord, pour clarifier le comportement "prévu": onSaveInstance et onRestoreInstance sont fragiles et uniquement pour les états transitoires. L’utilisation prévue (afaict) consiste à gérer la création d’activités lors de la rotation du téléphone (changement d’orientation). En d’autres termes, l’utilisation prévue est celle où votre activité est toujours logiquement «au-dessus», mais doit quand même être réinitialisée par le système. Le paquet sauvegardé n'est pas conservé en dehors du processus / mémoire / gc, vous ne pouvez donc pas vraiment vous fier à cela si votre activité passe en arrière-plan. Oui, peut-être que la mémoire de votre activité survivra à son voyage en arrière-plan et échappera au GC, mais cela n’est pas fiable (ni prévisible).

Ainsi, si vous avez un scénario où il existe une "progression de l'utilisateur" significative ou un état qui devrait être conservé entre les "lancements" de votre application, il est conseillé d'utiliser onPause et onResume. Vous devez choisir et préparer vous-même un magasin persistant.

MAIS - il y a un bug très déroutant qui complique tout cela. Les détails sont ici:

http://code.google.com/p/android/ issues / detail? id = 2373

http://code.google.com/p/android/ issues / detail? id = 5277

Fondamentalement, si votre application est lancée avec l'indicateur SingleTask et que vous la lancez ensuite à partir de l'écran d'accueil ou du menu du lanceur, cette invocation ultérieure créera une nouvelle tâche ... vous aurez effectivement deux instances différentes. de votre application habitant la même pile ... qui devient très étrange très vite. Cela semble se produire lorsque vous lancez votre application pendant le développement (à partir d’Eclipse ou d’Intellij), les développeurs doivent donc souvent faire face à cette situation. Mais aussi par le biais de certains mécanismes de mise à jour de l'App Store (cela impactera également vos utilisateurs).

Je me suis battu dans ces discussions pendant des heures avant de me rendre compte que mon problème principal était ce bogue, et non le comportement du framework souhaité. Une excellente écriture et solution de contournement (UPDATE: voir ci-dessous) semble provenir de l'utilisateur @kaciula dans cette réponse:

Comportement des touches de raccourci

UPDATE Juin 2013 : Quelques mois plus tard, j'ai enfin trouvé la solution "correcte". Vous n'avez pas besoin de gérer vous-même les drapeaux startedApp Stateful, vous pouvez le détecter à partir de la structure et le libérer correctement. Je l’utilise près du début de mon LauncherActivity.onCreate:

if (!isTaskRoot()) {
    Intent intent = getIntent();
    String action = intent.getAction();
    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && action != null && action.equals(Intent.ACTION_MAIN)) {
        finish();
        return;
    }
}

onSaveInstanceState est appelé lorsque le système a besoin de mémoire et tue une application. Il n'est pas appelé lorsque l'utilisateur ferme simplement l'application. Je pense donc que l'état de l'application doit également être enregistré dans onPause . Il doit être enregistré dans un stockage persistant tel que Préférences ou SQL

.

Les deux méthodes sont utiles et valides, et conviennent parfaitement à différents scénarios:

  1. L'utilisateur ferme l'application et la rouvre ultérieurement, mais elle doit recharger les données de la dernière session. Cette méthode nécessite une approche de stockage persistant, telle que l'utilisation de SQLite.
  2. L'utilisateur change d'application, puis revient à l'original et souhaite reprendre là où il en était resté - sauvegarde et restauration des données d'ensemble (telles que les données d'état de l'application) dans onSaveInstanceState () et onRestoreInstanceState () est généralement suffisant.

Si vous enregistrez les données d'état de manière persistante, elles peuvent être rechargées dans un onResume () ou un onCreate () (ou en réalité lors de tout appel de cycle de vie). Ce comportement peut ou peut ne pas être souhaité. Si vous le stockez dans un paquet dans un InstanceState , il est transitoire et ne convient que pour stocker des données destinées à être utilisées dans le même utilisateur 'session' (j'utilise le terme session vaguement) mais pas entre ' sessions '.

Ce n’est pas qu’une approche soit meilleure que l’autre, mais il est tout aussi important de comprendre le comportement que vous souhaitez et de choisir l’approche la plus appropriée.

En ce qui me concerne, la sauvegarde est au mieux un kludge. Si vous devez enregistrer des données persistantes, utilisez simplement une base de données SQLite . Android facilite SOOO .

Quelque chose comme ça:

import java.util.Date;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class dataHelper {

    private static final String DATABASE_NAME = "autoMate.db";
    private static final int DATABASE_VERSION = 1;

    private Context context;
    private SQLiteDatabase db;
    private OpenHelper oh ;

    public dataHelper(Context context) {
        this.context = context;
        this.oh = new OpenHelper(this.context);
        this.db = oh.getWritableDatabase();
    }

    public void close() {
        db.close();
        oh.close();
        db = null;
        oh = null;
        SQLiteDatabase.releaseMemory();
    }


    public void setCode(String codeName, Object codeValue, String codeDataType) {
        Cursor codeRow = db.rawQuery("SELECT * FROM code WHERE codeName = '"+  codeName + "'", null);
        String cv = "" ;

        if (codeDataType.toLowerCase().trim().equals("long") == true){
            cv = String.valueOf(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("int") == true)
        {
            cv = String.valueOf(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("date") == true)
        {
            cv = String.valueOf(((Date)codeValue).getTime());
        }
        else if (codeDataType.toLowerCase().trim().equals("boolean") == true)
        {
            String.valueOf(codeValue);
        }
        else
        {
            cv = String.valueOf(codeValue);
        }

        if(codeRow.getCount() > 0) //exists-- update
        {
            db.execSQL("update code set codeValue = '" + cv +
                "' where codeName = '" + codeName + "'");
        }
        else // does not exist, insert
        {
            db.execSQL("INSERT INTO code (codeName, codeValue, codeDataType) VALUES(" +
                    "'" + codeName + "'," +
                    "'" + cv + "'," +
                    "'" + codeDataType + "')" );
        }
    }

    public Object getCode(String codeName, Object defaultValue){

        //Check to see if it already exists
        String codeValue = "";
        String codeDataType = "";
        boolean found = false;
        Cursor codeRow  = db.rawQuery("SELECT * FROM code WHERE codeName = '"+  codeName + "'", null);
        if (codeRow.moveToFirst())
        {
            codeValue = codeRow.getString(codeRow.getColumnIndex("codeValue"));
            codeDataType = codeRow.getString(codeRow.getColumnIndex("codeDataType"));
            found = true;
        }

        if (found == false)
        {
            return defaultValue;
        }
        else if (codeDataType.toLowerCase().trim().equals("long") == true)
        {
            if (codeValue.equals("") == true)
            {
                return (long)0;
            }
            return Long.parseLong(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("int") == true)
        {
            if (codeValue.equals("") == true)
            {
                return (int)0;
            }
            return Integer.parseInt(codeValue);
        }
        else if (codeDataType.toLowerCase().trim().equals("date") == true)
        {
            if (codeValue.equals("") == true)
            {
                return null;
            }
            return new Date(Long.parseLong(codeValue));
        }
        else if (codeDataType.toLowerCase().trim().equals("boolean") == true)
        {
            if (codeValue.equals("") == true)
            {
                return false;
            }
            return Boolean.parseBoolean(codeValue);
        }
        else
        {
            return (String)codeValue;
        }
    }


    private static class OpenHelper extends SQLiteOpenHelper {

        OpenHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE IF  NOT EXISTS code" +
            "(id INTEGER PRIMARY KEY, codeName TEXT, codeValue TEXT, codeDataType TEXT)");
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }
    }
}

Un simple appel après cela

dataHelper dh = new dataHelper(getBaseContext());
String status = (String) dh.getCode("appState", "safetyDisabled");
Date serviceStart = (Date) dh.getCode("serviceStartTime", null);
dh.close();
dh = null;

Je pense avoir trouvé la réponse. Laissez-moi vous raconter ce que j'ai fait avec des mots simples:

Supposons que j’ai deux activités, activité1 et activité2 et que je navigue d’une activité1 à une activité2 (j’ai effectué quelques travaux dans l’activité2), puis que je retourne à l’activité 1 en cliquant sur un bouton de l’activité1. Maintenant, à ce stade, je voulais revenir à activity2 et je veux voir mon activité2 dans le même état que la dernière fois que j'ai quitté activity2.

Pour le scénario ci-dessus, ce que j'ai fait est que, dans le manifeste, j'ai apporté des modifications telles que:

<activity android:name=".activity2"
          android:alwaysRetainTaskState="true"      
          android:launchMode="singleInstance">
</activity>

Et dans l'activité1 sur l'événement de clic de bouton, je l'ai fait comme suit:

Intent intent = new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.setClassName(this,"com.mainscreen.activity2");
startActivity(intent);

Et dans activity2 sur l'événement de clic de bouton, je l'ai fait comme ceci:

Intent intent=new Intent();
intent.setClassName(this,"com.mainscreen.activity1");
startActivity(intent);

Maintenant, ce qui va arriver, c’est que les modifications apportées à l’activité2 ne seront pas perdues et que nous pourrons voir l’activité2 dans le même état que nous avons quitté précédemment.

Je crois que c'est la réponse et que cela fonctionne bien pour moi. Corrigez-moi si je me trompe.

onSaveInstanceState () pour les données transitoires (restauré dans onCreate () / onRestoreInstanceState () ), onPause () pour les données persistantes (restauré dans onResume () ). À partir des ressources techniques Android:

  

onSaveInstanceState () est appelé par Android si l'activité est en cours d'arrêt et peut être tué avant d'être repris! Cela signifie qu'il doit stocker tout état nécessaire pour se réinitialiser dans la même condition lorsque l'activité est redémarrée. Il s'agit de la contrepartie de la méthode onCreate (). En fait, le paquet savedInstanceState transmis à onCreate () est le même paquet que vous construisez comme outState dans la méthode onSaveInstanceState ().

     

onPause () et onResume () sont également des méthodes complémentaires. onPause () est toujours appelé à la fin de l'activité, même si nous l'initiions (avec un appel à finish () par exemple). Nous allons l'utiliser pour enregistrer la note actuelle dans la base de données. La bonne pratique consiste à libérer toutes les ressources pouvant être libérées au cours d’une onPause () également, afin de consommer moins de ressources dans un état passif.

Vraiment onSaveInstance appelle l'état lorsque l'activité passe en arrière-plan

Citation de la documentation: "La méthode onSaveInstanceState (Bundle) est appelée avant de placer l'activité dans un tel état d'arrière-plan"

Pour réduire les problèmes, j'utilise les interfaces et les classes suivants pour lire / écrire dans un Bundle afin de sauvegarder l'état de l'instance.

Tout d'abord, créez une interface qui sera utilisée pour annoter vos variables d'instance:

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.FIELD
})
public @interface SaveInstance {

}

Créez ensuite une classe dans laquelle la réflexion sera utilisée pour enregistrer des valeurs dans le bundle:

import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;

import java.io.Serializable;
import java.lang.reflect.Field;

/**
 * Save and load fields to/from a {@link Bundle}. All fields should be annotated with {@link
 * SaveInstance}.</p>
 */
public class Icicle {

    private static final String TAG = "Icicle";

    /**
     * Find all fields with the {@link SaveInstance} annotation and add them to the {@link Bundle}.
     *
     * @param outState
     *         The bundle from {@link Activity#onSaveInstanceState(Bundle)} or {@link
     *         Fragment#onSaveInstanceState(Bundle)}
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @see #load(Bundle, Object)
     */
    public static void save(Bundle outState, Object classInstance) {
        save(outState, classInstance, classInstance.getClass());
    }

    /**
     * Find all fields with the {@link SaveInstance} annotation and add them to the {@link Bundle}.
     *
     * @param outState
     *         The bundle from {@link Activity#onSaveInstanceState(Bundle)} or {@link
     *         Fragment#onSaveInstanceState(Bundle)}
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @param baseClass
     *         Base class, used to get all superclasses of the instance.
     * @see #load(Bundle, Object, Class)
     */
    public static void save(Bundle outState, Object classInstance, Class<?> baseClass) {
        if (outState == null) {
            return;
        }
        Class<?> clazz = classInstance.getClass();
        while (baseClass.isAssignableFrom(clazz)) {
            String className = clazz.getName();
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(SaveInstance.class)) {
                    field.setAccessible(true);
                    String key = className + "#" + field.getName();
                    try {
                        Object value = field.get(classInstance);
                        if (value instanceof Parcelable) {
                            outState.putParcelable(key, (Parcelable) value);
                        } else if (value instanceof Serializable) {
                            outState.putSerializable(key, (Serializable) value);
                        }
                    } catch (Throwable t) {
                        Log.d(TAG, "The field '" + key + "' was not added to the bundle");
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

    /**
     * Load all saved fields that have the {@link SaveInstance} annotation.
     *
     * @param savedInstanceState
     *         The saved-instance {@link Bundle} from an {@link Activity} or {@link Fragment}.
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @see #save(Bundle, Object)
     */
    public static void load(Bundle savedInstanceState, Object classInstance) {
        load(savedInstanceState, classInstance, classInstance.getClass());
    }

    /**
     * Load all saved fields that have the {@link SaveInstance} annotation.
     *
     * @param savedInstanceState
     *         The saved-instance {@link Bundle} from an {@link Activity} or {@link Fragment}.
     * @param classInstance
     *         The object to access the fields which have the {@link SaveInstance} annotation.
     * @param baseClass
     *         Base class, used to get all superclasses of the instance.
     * @see #save(Bundle, Object, Class)
     */
    public static void load(Bundle savedInstanceState, Object classInstance, Class<?> baseClass) {
        if (savedInstanceState == null) {
            return;
        }
        Class<?> clazz = classInstance.getClass();
        while (baseClass.isAssignableFrom(clazz)) {
            String className = clazz.getName();
            for (Field field : clazz.getDeclaredFields()) {
                if (field.isAnnotationPresent(SaveInstance.class)) {
                    String key = className + "#" + field.getName();
                    field.setAccessible(true);
                    try {
                        Object fieldVal = savedInstanceState.get(key);
                        if (fieldVal != null) {
                            field.set(classInstance, fieldVal);
                        }
                    } catch (Throwable t) {
                        Log.d(TAG, "The field '" + key + "' was not retrieved from the bundle");
                    }
                }
            }
            clazz = clazz.getSuperclass();
        }
    }

}

Exemple d'utilisation:

public class MainActivity extends Activity {

    @SaveInstance
    private String foo;

    @SaveInstance
    private int bar;

    @SaveInstance
    private Intent baz;

    @SaveInstance
    private boolean qux;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Icicle.load(savedInstanceState, this);
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Icicle.save(outState, this);
    }

}

Remarque: Ce code a été adapté à partir d'un projet de bibliothèque nommé AndroidAutowire , qui est sous la licence < a href = "https://raw.githubusercontent.com/CardinalNow/AndroidAutowire/master/LICENSE"> Licence MIT .

En attendant, en général, je n’utilise plus

Bundle savedInstanceState & Co

Pour la plupart des activités, le cycle de vie est trop compliqué et inutile.

Et Google s’affirme lui-même, ce n’est même pas fiable.

Mon moyen consiste à enregistrer immédiatement toutes les modifications dans les préférences:

 SharedPreferences p;
 p.edit().put(..).commit()

D'une certaine manière, les préférences partagées fonctionnent de la même manière que les ensembles. Et naturellement et au début, ces valeurs doivent être lues à partir des préférences.

Dans le cas de données complexes, vous pouvez utiliser SQLite au lieu d'utiliser les préférences.

Lors de l'application de ce concept, l'activité continue d'utiliser le dernier état enregistré, qu'il s'agisse d'une ouverture initiale avec redémarrage intermédiaire ou d'une réouverture en raison de la pile arrière.

Pour répondre directement à la question d'origine. savedInstancestate est null car votre activité n'est jamais recréée.

Votre activité ne sera recréée avec un groupe d'états que:

  • Des modifications de configuration, telles que la modification de l'orientation ou de la langue du téléphone, pouvant nécessiter la création d'une nouvelle instance d'activité.
  • Vous revenez à l'application de l'arrière-plan après que le système d'exploitation a détruit l'activité.

Android détruira les activités en arrière-plan en cas de pression de la mémoire ou après une longue période en arrière-plan.

Lorsque vous testez votre exemple hello world, il existe plusieurs façons de quitter et de revenir à l'activité.

  • Lorsque vous appuyez sur le bouton Précédent, l'activité est terminée. La relance de l'application est une toute nouvelle instance. Vous ne reprenez pas du tout l'arrière-plan.
  • Lorsque vous appuyez sur le bouton d'accueil ou utilisez le sélecteur de tâches, l'activité passe à l'arrière-plan. Lors de la navigation vers l'application, onCreate ne sera appelé que si l'activité doit être détruite.

Dans la plupart des cas, si vous appuyez simplement sur la touche d'accueil puis relancez l'application, l'activité n'aura pas besoin d'être recréée. Il existe déjà en mémoire, donc onCreate () ne sera pas appelé.

Il y a une option sous Paramètres - > Les options pour les développeurs sont appelées "Ne pas conserver les activités". Lorsqu'il est activé, Android détruira toujours les activités et les recréera une fois qu'elles seront en arrière-plan. C’est une excellente option à laisser activée lors du développement car elle simule le pire des scénarios. (Un périphérique à faible mémoire recyclant vos activités tout le temps).

Les autres réponses sont précieuses car elles vous apprennent les bonnes manières de stocker l'état, mais je ne pensais pas qu'elles répondaient vraiment POURQUOI votre code ne fonctionnait pas comme prévu.

Les méthodes onSaveInstanceState (bundle) et onRestoreInstanceState (bundle) sont utiles pour la persistance des données simplement en faisant pivoter l'écran (changement d'orientation).
Ils ne sont même pas bons lors du basculement entre les applications (puisque la méthode onSaveInstanceState () est appelée mais que onCreate (bundle) et onRestoreInstanceState (bundle) est non invoqué à nouveau.
Pour plus de persistance, utilisez les préférences partagées. lire cet article

Mon problème était que je n'avais besoin de persistance que pendant la durée de vie de l'application (c'est-à-dire une exécution unique comprenant le démarrage d'autres sous-activités au sein de la même application et la rotation de l'appareil, etc.). J'ai essayé diverses combinaisons des réponses ci-dessus mais je n'ai pas obtenu ce que je voulais dans toutes les situations. Finalement, ce qui a fonctionné pour moi a été d’obtenir une référence à savedInstanceState lors de onCreate:

mySavedInstanceState=savedInstanceState;

et utilisez-le pour obtenir le contenu de ma variable lorsque j'en ai besoin, comme suit:

if (mySavedInstanceState !=null) {
   boolean myVariable = mySavedInstanceState.getBoolean("MyVariable");
}

J'utilise onSaveInstanceState et onRestoreInstanceState comme suggéré ci-dessus, mais je suppose que je pourrais aussi ou alternativement utiliser ma méthode pour enregistrer la variable lorsqu'elle change (par exemple, en utilisant putBoolean )

Bien que la réponse acceptée soit correcte, il existe une méthode plus rapide et plus simple pour enregistrer l'état de l'activité sur Android à l'aide d'une bibliothèque appelée Icepick . Icepick est un processeur d'annotation qui prend en charge tout le code standard utilisé pour la sauvegarde et la restauration de l'état pour vous.

Faire quelque chose comme ça avec Icepick:

class MainActivity extends Activity {
  @State String username; // These will be automatically saved and restored
  @State String password;
  @State int age;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Override public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }
}

Est-ce la même chose que faire ceci:

class MainActivity extends Activity {
  String username;
  String password;
  int age;

  @Override
  public void onSaveInstanceState(Bundle savedInstanceState) {
    super.onSaveInstanceState(savedInstanceState);
    savedInstanceState.putString("MyString", username);
    savedInstanceState.putString("MyPassword", password);
    savedInstanceState.putInt("MyAge", age); 
    /* remember you would need to actually initialize these variables before putting it in the
    Bundle */
  }

  @Override
  public void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    username = savedInstanceState.getString("MyString");
    password = savedInstanceState.getString("MyPassword");
    age = savedInstanceState.getInt("MyAge");
  }
}

Icepick fonctionnera avec tout objet qui enregistre son état avec un Bundle .

Quand une activité est créée, sa méthode onCreate () est appelée.

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

savedInstanceState est un objet de la classe Bundle qui est null pour la première fois, mais il contient des valeurs lorsqu'il est recréé. Pour enregistrer l'état de l'activité, vous devez remplacer onSaveInstanceState ().

   @Override
    protected void onSaveInstanceState(Bundle outState) {
      outState.putString("key","Welcome Back")
        super.onSaveInstanceState(outState);       //save state
    }

indiquez vos valeurs dans "outState". Regroupez les objets comme outState.putString ("clé", "Welcome Back") et enregistrez-les en appelant super. Lorsque l'activité est détruite, son état est enregistré dans l'objet Bundle et peut être restauré après une recréation dans onCreate () ou onRestoreInstanceState (). Les ensembles reçus dans onCreate () et onRestoreInstanceState () sont identiques.

   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

          //restore activity's state
         if(savedInstanceState!=null){
          String reStoredString=savedInstanceState.getString("key");
            }
    }

ou

  //restores activity's saved state
 @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
      String restoredMessage=savedInstanceState.getString("key");
    }

Il existe fondamentalement deux manières d'appliquer ce changement.

  1. utilisant onSaveInstanceState () et onRestoreInstanceState () .
  2. Dans le manifeste android: configChanges = "orientation | screenSize" .

Je ne recommande vraiment pas l’utilisation de la seconde méthode. D'après l'une de mes expériences, la moitié de l'écran de l'appareil était noir lors du basculement d'un portrait à l'autre et inversement.

En utilisant la première méthode mentionnée ci-dessus, nous pouvons conserver les données lorsque l'orientation est modifiée ou que tout changement de configuration se produit. Je connais un moyen de stocker n'importe quel type de données dans un objet d'état savedInstance.

Exemple: considérons un cas si vous souhaitez conserver un objet Json. créer une classe de modèle avec des getters et des setters.

class MyModel extends Serializable{
JSONObject obj;

setJsonObject(JsonObject obj)
{
this.obj=obj;
}

JSONObject getJsonObject()
return this.obj;
} 
}

Maintenant, dans votre activité dans les méthodes onCreate et onSaveInstanceState, procédez comme suit. Cela ressemblera à ceci:

@override
onCreate(Bundle savedInstaceState){
MyModel data= (MyModel)savedInstaceState.getSerializable("yourkey")
JSONObject obj=data.getJsonObject();
//Here you have retained JSONObject and can use.
}


@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//Obj is some json object 
MyModel dataToSave= new MyModel();
dataToSave.setJsonObject(obj);
oustate.putSerializable("yourkey",dataToSave); 

}

Voici un commentaire de la réponse de Steve Moseley (par ToolmakerSteve ) qui met les choses en perspective (dans l'ensemble onSaveInstanceState vs OnPause, saga East Cost vs West Cost )

  

@VVK - Je suis partiellement en désaccord. Certaines manières de quitter une application ne se déclenchent pas   onSaveInstanceState (oSIS). Cela limite l'utilité de oSIS. Ses   Il vaut la peine de supporter, pour des ressources minimales du système d'exploitation, mais si une application veut   ramène l'utilisateur à l'état dans lequel il se trouvait, quelle que soit l'application   Pour finir, il est nécessaire d’utiliser une approche de stockage persistant.    J'utilise onCreate pour rechercher une offre groupée. Si elle est manquante, vérifiez .    stockage persistant. : cela centralise la prise de décision. je peux   récupérer après un plantage, ou quitter le bouton Précédent ou l’élément de menu personnalisé Quitter, ou   revenir à l'écran utilisateur était sur plusieurs jours plus tard. & # 8211; ToolmakerSteve Sep   19 '15 à 10h38

Code Kotlin:

enregistrer:

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState.apply {
        putInt("intKey", 1)
        putString("stringKey", "String Value")
        putParcelable("parcelableKey", parcelableObject)
    })
}

puis dans onCreate () ou onRestoreInstanceState ()

    val restoredInt = savedInstanceState?.getInt("intKey") ?: 1 //default int
    val restoredString = savedInstanceState?.getString("stringKey") ?: "default string"
    val restoredParcelable = savedInstanceState?.getParcelable<ParcelableClass>("parcelableKey") ?: ParcelableClass() //default parcelable

Ajoutez des valeurs par défaut si vous ne voulez pas avoir d'options (optionnelles)

Pour obtenir les données d'état d'activité stockées dans onCreate () , vous devez d'abord enregistrer les données dans le fichier savedInstanceState en remplaçant la méthode SaveInstanceState (Bundle savedInstanceState) .

Lorsque l'activité détruit, la méthode SaveInstanceState (Bundle savedInstanceState) est appelée et vous enregistrez les données que vous souhaitez enregistrer. Et vous obtenez la même chose dans onCreate () lorsque l'activité redémarre. (SavedInstanceState ne sera pas nul, car vous y avez enregistré des données avant que l'activité ne soit détruite)

Pour résoudre ce problème, simple et rapide, utilisez IcePick

.

Tout d'abord, configurez la bibliothèque dans app / build.gradle

repositories {
  maven {url "https://clojars.org/repo/"}
}
dependencies {
  compile 'frankiesardo:icepick:3.2.0'
  provided 'frankiesardo:icepick-processor:3.2.0'
}

Maintenant, regardons cet exemple ci-dessous comment enregistrer un état dans l'activité

public class ExampleActivity extends Activity {
  @State String username; // This will be automatically saved and restored

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Icepick.restoreInstanceState(this, savedInstanceState);
  }

  @Override public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    Icepick.saveInstanceState(this, outState);
  }
}

Cela fonctionne pour les activités, les fragments ou tout objet devant sérialiser son état sur un ensemble (par exemple, ViewPresenters de mortier)

Icepick peut également générer le code d'état d'instance pour des vues personnalisées:

class CustomView extends View {
  @State int selectedPosition; // This will be automatically saved and restored

  @Override public Parcelable onSaveInstanceState() {
    return Icepick.saveInstanceState(this, super.onSaveInstanceState());
  }

  @Override public void onRestoreInstanceState(Parcelable state) {
    super.onRestoreInstanceState(Icepick.restoreInstanceState(this, state));
  }

  // You can put the calls to Icepick into a BaseCustomView and inherit from it
  // All Views extending this CustomView automatically have state saved/restored
}

Je ne suis pas sûr que ma solution soit mal vue ou non, mais j'utilise un service lié pour conserver l'état ViewModel. Que vous le stockiez en mémoire dans le service ou persistiez et récupériez-le à partir d'une base de données SQLite, cela dépend de vos besoins. C’est ce que font les services de toutes les saveurs, ils fournissent des services tels que la gestion de l’état des applications et la logique métier commune abstraite.

En raison des contraintes de mémoire et de traitement inhérentes aux appareils mobiles, je traite les vues Android de la même manière qu'une page Web. La page ne conserve pas l'état, il s'agit uniquement d'un composant de la couche de présentation dont le seul but est de présenter l'état de l'application et d'accepter les entrées de l'utilisateur. Les tendances récentes en matière d’architecture d’applications Web utilisent le modèle très ancien Modèle, Vue, Contrôleur (MVC), dans lequel la page correspond à la Vue, les données de domaine au modèle, et le contrôleur se cache derrière un service Web. Le même modèle peut être utilisé dans Android, la vue étant, ainsi ... la vue, le modèle correspond aux données de votre domaine et le contrôleur est implémenté en tant que service lié à Android. Chaque fois que vous souhaitez qu'une vue interagisse avec le contrôleur, liez-le au démarrage / reprise et dissociez-le à l'arrêt / la pause.

Cette approche vous donne l’avantage supplémentaire d’appliquer le principe de conception Séparation de préoccupations en ce sens que toute logique d’application peut être déplacée dans votre service, ce qui réduit la duplication de plusieurs vues et permet à la vue d’appliquer un autre principe de conception important, Responsabilité unique.

Maintenant, Android fournit ViewModels pour enregistrer l'état, vous devez essayer de l'utiliser à la place de saveInstanceState.

Que faut-il sauvegarder et que ne pas faire?

Vous êtes-vous déjà demandé pourquoi le texte de EditText est enregistré automatiquement lors du changement d'orientation? Eh bien, cette réponse est pour vous.

Lorsqu'une instance d'une activité est détruite et que le système recrée une nouvelle instance (par exemple, modification de la configuration). Il tente de le recréer à l'aide d'un ensemble de données sauvegardées de l'ancien état d'activité ( état d'instance ).

état d'instance est une collection de paires clé-valeur stockées dans un objet Bundle .

  

Par défaut, le système enregistre les objets View du lot par exemple.

  • Texte dans EditText
  • Faites défiler la position dans un ListView , etc.

Si vous avez besoin de sauvegarder une autre variable en tant qu'élément de l'état de l'instance, vous devez utiliser la méthode OVERRIDE onSavedInstanceState (ensemble Bundle savedinstaneState) .

Par exemple, int currentScore dans une GameActivity

Plus de détails sur onSavedInstanceState (ensemble sauvegardé dans état) lors de la sauvegarde de données

@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
    // Save the user's current game state
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);

    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(savedInstanceState);
}
  

Donc, par erreur, si vous oubliez d'appeler    super.onSaveInstanceState (savedInstanceState); le comportement par défaut   ne fonctionnera pas, c’est-à-dire que le texte dans EditText ne sera pas sauvegardé.

Lequel choisir pour restaurer l'état d'activité?

 onCreate(Bundle savedInstanceState)

OU

onRestoreInstanceState(Bundle savedInstanceState)

Les deux méthodes obtiennent le même objet Bundle. Par conséquent, le lieu où vous écrivez votre logique de restauration n'a pas d'importance. La seule différence est que, dans la méthode onCreate (Bundle savedInstanceState) , vous devrez effectuer une vérification nulle alors qu'elle n'est pas nécessaire dans ce dernier cas. D'autres réponses ont déjà des extraits de code. Vous pouvez les référer.

Plus de détails sur onRestoreInstanceState (ensemble enregistré dans état>)

@Override
public void onRestoreInstanceState(Bundle savedInstanceState) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState);

    // Restore state members from the saved instance
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
}
  

Appelez toujours super.onRestoreInstanceState (savedInstanceState); pour que le système restaure la hiérarchie des vues par défaut

Bonus

Le onSaveInstanceState (Bundle savedInstanceState) n'est appelé par le système que lorsque l'utilisateur a l'intention de revenir à l'activité. Par exemple, vous utilisez App X et vous recevez un appel. Vous accédez à l'application appelant et revenez à l'application X. Dans ce cas, la méthode onSaveInstanceState (Bundle savedInstanceState) sera appelée.

Mais considérez ceci si un utilisateur appuie sur le bouton Précédent. Il est supposé que l'utilisateur n'a pas l'intention de revenir à l'activité. Par conséquent, dans ce cas, onSaveInstanceState (Bundle savedInstanceState) ne sera pas appelé par le système. Le point étant que vous devriez considérer tous les scénarios lors de la sauvegarde des données.

Liens pertinents:

Démo sur le comportement par défaut
Documentation officielle Android .

Kotlin

Vous devez remplacer onSaveInstanceState et onRestoreInstanceState pour stocker et récupérer les variables que vous souhaitez conserver.

Graphique du cycle de vie

Stocker les variables

public override fun onSaveInstanceState(savedInstanceState: Bundle) {
    super.onSaveInstanceState(savedInstanceState)

    // prepare variables here
    savedInstanceState.putInt("kInt", 10)
    savedInstanceState.putBoolean("kBool", true)
    savedInstanceState.putDouble("kDouble", 4.5)
    savedInstanceState.putString("kString", "Hello Kotlin")
}

Récupérer les variables

public override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)

    val myInt = savedInstanceState.getInt("kInt")
    val myBoolean = savedInstanceState.getBoolean("kBool")
    val myDouble = savedInstanceState.getDouble("kDouble")
    val myString = savedInstanceState.getString("kString")
    // use variables here
}

J'ai une meilleure idée. Ce serait mieux de sauvegarder vos données sans appeler onCreate. Vous pouvez le désactiver d'activité lorsque l'orientation change.

Dans votre manifeste:

<activity android:name=".MainActivity"
        android:configChanges="orientation|screenSize">

Modifier: Cette solution ne fonctionnera pas si Android tue le processus en raison d'une mémoire insuffisante. si vous devez conserver des données avec certitude, vous devez utiliser SavedInstanceState dans ce cas, sinon je suggère d'utiliser cette méthode. cool et facile à utiliser!

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top