Как сохранить состояние активности Android с помощью сохранения состояния экземпляра? - PullRequest
2425 голосов
/ 30 сентября 2008

Я работаю на платформе Android SDK, и немного неясно, как сохранить состояние приложения. Итак, учитывая этот незначительный повторный инструментарий примера «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);
  }
}

Я думал, что этого будет достаточно для самого простого случая, но он всегда отвечает первым сообщением, независимо от того, как я ухожу из приложения.

Я уверен, что решение так же просто, как переопределение onPause или что-то в этом роде, но я копался в документации около 30 минут и не нашел ничего очевидного.

Ответы [ 28 ]

2423 голосов
/ 30 сентября 2008

Вам необходимо переопределить onSaveInstanceState(Bundle savedInstanceState) и записать значения состояния приложения, которые вы хотите изменить, в параметр Bundle следующим образом:

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

Bundle - это, по сути, способ хранения карты NVP («пара имя-значение»), и он передается в onCreate(), а также onRestoreInstanceState(), где вы затем извлекаете значения следующим образом:

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

Обычно вы используете эту технику для хранения значений экземпляра для вашего приложения (выборки, несохраненный текст и т. Д.).

406 голосов
/ 30 сентября 2008

savedInstanceState предназначен только для сохранения состояния, связанного с текущим экземпляром Действия, например, текущей навигацией или информацией о выборе, так что, если Android уничтожает и воссоздает Деятельность, он может вернуться, как и прежде. См. Документацию для onCreate и onSaveInstanceState

Для более долгоживущего состояния рассмотрите возможность использования базы данных SQLite, файла или настроек. См. Сохранение постоянного состояния .

392 голосов
/ 26 мая 2010

Обратите внимание, что NOT безопасно использовать onSaveInstanceState и onRestoreInstanceState для постоянных данных , согласно документации по состояниям активности в http://developer.android.com/reference/android/app/Activity.html.

В документе указано (в разделе «Жизненный цикл действия»):

Обратите внимание, что важно сохранить постоянные данные в onPause() вместо из onSaveInstanceState(Bundle) потому что позже не является частью жизненных циклов обратных вызовов, поэтому не будет вызывается в любой ситуации, как описано в своей документации.

Другими словами, введите код сохранения / восстановления для постоянных данных в onPause() и onResume()!

РЕДАКТИРОВАТЬ : Для дальнейшего разъяснения, вот документация onSaveInstanceState():

Этот метод вызывается до того, как действие может быть убито, так что когда оно возвращается в какое-то время в будущем, он может восстановить свое состояние. За Например, если действие B запускается перед действием A, а в некоторых Точка активности А убита, чтобы вернуть ресурсы, деятельность А будет иметь возможность сохранить текущее состояние своего пользовательского интерфейса через этот метод, так что когда пользователь возвращается к действию А, состояние пользовательский интерфейс может быть восстановлен через onCreate(Bundle) или onRestoreInstanceState(Bundle).

187 голосов
/ 27 августа 2010

Мой коллега написал статью, объясняющую состояние приложения на устройствах Android, включая объяснения жизненного цикла активности и информации о состоянии, как хранить информацию о состоянии, а также сохранение в состояние Bundle и SharedPreferences и , посмотрите здесь .

В статье рассматриваются три подхода:

Сохранение локальных переменных / данных управления пользовательским интерфейсом для времени жизни приложения (т.е. временно) с использованием пакета состояния экземпляра

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

Хранить данные локальной переменной / управления пользовательским интерфейсом между экземплярами приложения (т.е. постоянно) с использованием общих настроек

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

Сохранение экземпляров объекта в памяти между действиями в течение времени жизни приложения с использованием сохраненного экземпляра без конфигурации

[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();
}
137 голосов
/ 20 октября 2012

Это классическая «черта» разработки для Android. Здесь есть две проблемы:

  • Существует небольшая ошибка в Android Framework, которая значительно усложняет управление стеком приложений во время разработки, по крайней мере, в старых версиях (не совсем уверенно, если / когда / как это было исправлено). Я расскажу об этой ошибке ниже.
  • «Нормальный» или предполагаемый способ решения этой проблемы сам по себе довольно сложен из-за двойственности onPause / onResume и onSaveInstanceState / onRestoreInstanceState

Просматривая все эти потоки, я подозреваю, что большую часть времени разработчики говорят об этих двух разных проблемах одновременно ... отсюда вся путаница и сообщения о том, что "это не работает для меня".

Во-первых, чтобы прояснить «предполагаемое» поведение: onSaveInstance и onRestoreInstance хрупки и только для переходного состояния. Предполагаемое использование (afaict) состоит в том, чтобы обрабатывать Активный отдых, когда телефон поворачивается (изменение ориентации). Другими словами, предполагаемое использование - это когда ваша активность все еще логически «наверху», но все равно должна быть переоценена системой. Сохраненный Bundle не сохраняется вне процесса / memory / gc, поэтому вы не можете действительно полагаться на это, если ваша деятельность переходит в фоновый режим. Да, возможно, память вашей Деятельности переживет свое путешествие в фоновый режим и избежит GC, но это ненадежно (и не предсказуемо).

Так что, если у вас есть сценарий, в котором есть значимый «прогресс пользователя» или состояние, которое должно сохраняться между «запусками» вашего приложения, руководство должно использовать onPause и onResume. Вы должны выбрать и подготовить постоянный магазин самостоятельно.

НО - есть очень запутанная ошибка, которая усложняет все это. Подробности здесь:

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

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

В основном, если ваше приложение запускается с флагом SingleTask, а затем вы запускаете его из главного экрана или из меню запуска, то этот последующий вызов создаст НОВУЮ задачу ... у вас фактически будет два разных экземпляра вашего приложения, в котором находится один и тот же стек ... что очень странно и очень быстро. Похоже, это происходит, когда вы запускаете свое приложение во время разработки (например, из Eclipse или Intellij), поэтому разработчики часто сталкиваются с этим. Но также через некоторые механизмы обновления магазина приложений (так что это также влияет на ваших пользователей).

Я боролся с этими потоками в течение нескольких часов, прежде чем понял, что моей главной проблемой была эта ошибка, а не предполагаемое поведение платформы. Отличная рецензия и обходной путь (ОБНОВЛЕНИЕ: см. Ниже), кажется, от пользователя @kaciula в этом ответе:

Поведение нажатия клавиши «Домой»

ОБНОВЛЕНИЕ Июнь 2013 : Несколько месяцев спустя я наконец нашел «правильное» решение. Вам не нужно управлять какими-либо флагами StarApp с сохранением состояния самостоятельно, вы можете обнаружить это из фреймворка и соответствующим образом внести залог. Я использую это в начале моего 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;
    }
}
77 голосов
/ 07 мая 2010

onSaveInstanceState вызывается, когда системе требуется память и убивает приложение. Он не вызывается, когда пользователь просто закрывает приложение. Поэтому я думаю, что состояние приложения также должно быть сохранено в onPause. Оно должно быть сохранено в некотором постоянном хранилище, например Preferences или Sqlite

66 голосов
/ 27 июня 2011

Оба метода полезны и допустимы, и оба лучше всего подходят для различных сценариев:

  1. Пользователь завершает работу приложения и открывает его позднее, но приложению необходимо перезагрузить данные из последнего сеанса - для этого требуется постоянный подход к хранилищу, такой как использование SQLite.
  2. Пользователь переключает приложение, а затем возвращается к исходному и хочет выбрать, где он остановился - сохранить и восстановить данные пакета (например, данные состояния приложения) в onSaveInstanceState() и onRestoreInstanceState() обычно достаточно.

Если вы сохраняете данные о состоянии в постоянном режиме, они могут быть перезагружены в onResume() или onCreate() (или фактически при любом вызове жизненного цикла). Это может или не может быть желаемого поведения. Если вы храните его в пакете в InstanceState, то он временный и подходит только для хранения данных для использования в одной и той же пользовательской «сессии» (я использую термин «сессия»), но не между «сессиями».

Дело не в том, что один подход лучше другого, как все, просто важно понять, какое поведение вам требуется, и выбрать наиболее подходящий подход.

61 голосов
/ 23 июня 2011

Сохранение состояния - в лучшем случае клудж, насколько я понимаю. Если вам нужно сохранить постоянные данные, просто используйте базу данных SQLite . Android делает это СООО легко.

Примерно так:

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

Простой вызов после этого

dataHelper dh = new dataHelper(getBaseContext());
String status = (String) dh.getCode("appState", "safetyDisabled");
Date serviceStart = (Date) dh.getCode("serviceStartTime", null);
dh.close();
dh = null;
55 голосов
/ 05 февраля 2012

Я думаю, что нашел ответ. Позвольте мне рассказать, что я сделал, простыми словами:

Предположим, у меня есть два действия: действие 1 и действие 2, и я перехожу от действия 1 к занятию 2 (я выполнил некоторые работы в занятии 2) и снова возвращаюсь к занятию 1, нажав кнопку в занятии 1. Теперь на этом этапе я хотел вернуться к занятию2 и хочу, чтобы мое занятие2 было в том же состоянии, когда я последний раз оставлял занятие2.

Для описанного выше сценария я сделал следующее: в манифесте я сделал некоторые изменения, подобные этому:

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

И в активности1 на событие нажатия кнопки я сделал так:

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

А в активности2 при событии нажатия кнопки я сделал так:

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

Теперь, что произойдет, так это то, что любые изменения, которые мы внесли в действие 2, не будут потеряны, и мы сможем просмотреть действие 2 в том же состоянии, в котором мы вышли ранее.

Я верю, что это ответ, и он прекрасно работает для меня. Поправь меня, если я ошибаюсь.

40 голосов
/ 17 января 2012

onSaveInstanceState() для переходных данных (восстановлено в onCreate() / onRestoreInstanceState()), onPause() для постоянных данных (восстановлено в onResume()). Из технических ресурсов Android:

onSaveInstanceState () вызывается Android, если действие прекращается и может быть убито до его возобновления! Это означает, что в нем должно храниться любое состояние, необходимое для повторной инициализации до того же состояния при перезапуске действия. Это аналог метода onCreate (), и фактически Bundle сохраненного экземпляра, переданного в onCreate (), является тем же пакетом, который вы создаете как outState в методе onSaveInstanceState ().

onPause () и onResume () также являются дополнительными методами. onPause () всегда вызывается, когда заканчивается действие, даже если мы это спровоцировали (например, с помощью метода finish ()). Мы будем использовать это, чтобы сохранить текущую заметку обратно в базу данных. Хорошей практикой является освобождение любых ресурсов, которые также могут быть освобождены во время onPause (), чтобы они занимали меньше ресурсов в пассивном состоянии.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...