android-activity states中文 - 使用“保存實例狀態”保存Android活動狀態




application lifecycle (23)

onSaveInstance當活動進入後台時,確實說明了callen

從文檔引用:“ onSaveInstanceState(Bundle)在將活動置於這樣的背景狀態之前調用該方法”

我一直在研究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分鐘左右並且沒有找到任何明顯的東西。


onSaveInstanceState()用於瞬態數據(在onCreate() / onRestoreInstanceState()恢復), onPause()用於持久數據(在onResume()恢復)。 來自Android技術資源:

如果Activity被停止並且可能在恢復之前被殺死,則Android會調用onSaveInstanceState() !這意味著它應該存儲重新啟動Activity時重新初始化為相同條件所需的任何狀態。它與onCreate()方法相對應,實際上傳入onCreate()的savedInstanceState Bundle與onSaveInstanceState()方法中構造為outState的Bundle相同。

onPause()onResume()也是免費的方法。當Activity結束時總是調用onPause(),即使我們發起了這個(例如使用finish()調用)。我們將使用它將當前註釋保存回數據庫。好的做法是釋放在onPause()期間可以釋放的任何資源,以便在處於被動狀態時佔用更少的資源。


這兩種方法都是有用且有效的,並且最適合不同的場景:

  1. 用戶終止應用程序並在以後重新打開它,但應用程序需要從上一個會話重新加載數據 - 這需要持久的存儲方法,例如使用SQLite。
  2. 用戶切換應用程序然後回到原始狀態並想要從中斷處繼續 - 在onSaveInstanceState()onRestoreInstanceState()保存和恢復捆綁數據(例如應用程序狀態數據)通常就足夠了。

如果以持久方式保存狀態數據,則可以在onResume()onCreate()重新加載(或實際在任何生命週期調用中)。 這可能是也可能不是期望的行為。 如果將它存儲在InstanceState的捆綁包中,則它是瞬態的,僅適用於存儲用於同一用戶“會話”的數據(我使用術語會話鬆散),但不適用於“會話”之間。

並不是說一種方法比另一種方法更好,就像所有方法一樣,了解您需要的行為並選擇最合適的方法非常重要。


savedInstanceState僅用於保存與當前活動實例關聯的狀態,例如當前導航或選擇信息,因此,如果Android銷毀並重新創建活動,它可以像之前一樣返回。 請參閱onCreateonSaveInstanceState的文檔

對於更長壽的狀態,請考慮使用SQLite數據庫,文件或首選項。 請參見保存持久狀態


現在Android提供ViewModels來保存狀態,你應該嘗試使用它來代替saveInstanceState。


為了幫助減少樣板,我使用以下內容interfaceclass讀取/寫入以Bundle保存實例狀態。

首先,創建一個用於註釋實例變量的接口:

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 {

}

然後,創建一個類,其中將使用反射將值保存到包中:

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

}

用法示例:

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

}

注意:此代碼改編自名為AndroidAutowire的庫項目,該項目根據MIT許可證獲得許可


要獲取存儲的活動狀態數據onCreate(),首先必須通過覆蓋SaveInstanceState(Bundle savedInstanceState)方法將數據保存在savedInstanceState中。

SaveInstanceState(Bundle savedInstanceState)調用活動銷毀方法並在那裡保存要保存的數據時。並且onCreate()當活動重新啟動時你會得到相同的結果。(savedInstanceState不會為null,因為你在活動被銷毀之前已經保存了一些數據)


創建活動時,會調用onCreate()方法。

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

savedInstanceState是Bundle類的一個對象,它第一次為null,但在重新創建時包含值。要保存Activity的狀態,您必須覆蓋onSaveInstanceState()。

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

將您的值放在“outState”Bundle對像中,如outState.putString(“key”,“Welcome Back”)並通過調用super來保存。當活動被銷毀時,它的狀態將被保存在Bundle對像中,並且可以在onCreate()或onRestoreInstanceState()中重新創建後恢復。在onCreate()和onRestoreInstanceState()中收到的Bundle是相同的。

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

要么

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

直接回答原始問題。savedInstancestate為null,因為永遠不會重新創建Activity。

只有在以下情況下,才會使用州捆綁包重新創建您的活動:

  • 配置更改,例如更改方向或電話語言,這可能需要創建新的活動實例。
  • 操作系統銷毀活動後,您將從後台返回應用程序。

Android會在內存壓力下或在長時間處於後台後銷毀後台活動。

在測試hello world示例時,有幾種方法可以離開並返回Activity。

  • 當您按後退按鈕時,活動結束。重新啟動應用程序是一個全新的實例。你根本沒有從後台恢復。
  • 當您按主頁按鈕或使用任務切換器時,活動將進入後台。導航回應用程序時,只有在必須銷毀活動時才會調用onCreate。

在大多數情況下,如果您只是按下主頁然後再次啟動應用程序,則無需重新創建活動。它已經存在於內存中,因此不會調用onCreate()。

“設置” - >“開發者選項”下有一個名為“不要保留活動”的選項。當它啟用時,Android將始終銷毀活動並在它們背景時重新創建它們。這是在開發時保持啟用的一個很好的選項,因為它模擬了最壞的情況。(低內存設備一直在回收您的活動)。

其他答案是有價值的,因為他們教你正確的存儲狀態的方法,但我不覺得他們真的回答為什麼你的代碼沒有以你預期的方式工作。


您需要覆蓋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");
}

您通常會使用此技術來存儲應用程序的實例值(選擇,未保存的文本等)。


這是Android開發的經典“問題”。 這裡有兩個問題:

  • 有一個微妙的Android Framework錯誤,它使開發過程中的應用程序堆棧管理變得非常複雜,至少在舊版本上是這樣(不完全確定是否/何時/如何修復)。 我將在下面討論這個錯誤。
  • 管理此問題的“正常”或預期方式本身相當複雜,具有onPause / onResume和onSaveInstanceState / onRestoreInstanceState的二元性

瀏覽所有這些線程,我懷疑開發人員大多數時間同時討論這兩個不同的問題......因此所有混淆和報告“這對我不起作用”。

首先,澄清“預期”行為:onSaveInstance和onRestoreInstance是脆弱的,僅適用於瞬態。 預期用途(afaict)用於在電話旋轉(方向改變)時處理活動娛樂。 換句話說,預期的用法是當您的Activity仍然在邏輯上“在頂部”時,但仍然必須由系統重新實例化。 保存的Bundle不會在進程/ memory / gc之外保留,因此如果您的活動進入後台,則無法真正依賴它。 是的,也許你的Activity的記憶將在它的背景之旅中存活並逃脫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年6月 :幾個月後,我終於找到了“正確”的解決方案。 您不需要自己管理任何有狀態的startupApp標記,您可以從框架中檢測到這一點並適當地保釋。 我在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;
    }
}

我想我找到了答案。 讓我用簡單的話說出我的所作所為:

假設我有兩個活動,activity1和activity2,我從activity1導航到activity2(我已在activity2中完成了一些工作),再次通過單擊activity1中的按鈕返回到活動1。 現在在這個階段我想回到activity2,我想在上次離開activity2時看到我的activity2處於相同的狀態。

對於上面的場景,我所做的是在清單中我做了一些像這樣的更改:

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

在按鈕點擊事件的activity1中我做了這樣的事情:

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

在按鈕點擊事件的activity2中我做了這樣的事情:

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

現在將會發生的事情是,我們在activity2中所做的任何更改都不會丟失,我們可以在與之前離開的狀態相同的狀態下查看activity2。

我相信這是答案,這對我來說很好。 如果我錯了,請糾正我。


將LiveData(Android架構組件)添加到項目中

添加以下依賴項

implementation "android.arch.lifecycle:extensions:1.1.0"

LiveData接收觀察者並僅在數據更改處於STARTED或RESUMED狀態時通知它。與LiveData的好處是,當你的活動進入其他比任何國家已啟動會不會調用調用onChanged的方法觀測

private TextView mTextView;
private MutableLiveData<String> mMutableLiveData;

@Override
protected void onCreate(Bundle savedInstanceState) {
    mTextView = (TextView) findViewById(R.id.textView);
    mMutableLiveData = new MutableLiveData<>();
    mMutableLiveData.observe(this, new Observer<String>() {
        @Override
        public void onChanged(@Nullable String s) {
            mTextView.setText(s);
        }
    });

}

實現此更改基本上有兩種方法。

  1. 使用onSaveInstanceState()onRestoreInstanceState()
  2. 在清單中android:configChanges="orientation|screenSize"

我真的不建議使用第二種方法。因為在我的一次經驗中,它導致設備屏幕的一半在從縱向旋轉到橫向時變黑,反之亦然。

使用上面提到的第一種方法,我們可以在方向更改或任何配置更改發生時保留數據。我知道一種可以在savedInstance狀態對像中存儲任何類型數據的方法。

示例:如果要保留Json對象,請考慮一個案例。使用getter和setter創建一個模型類。

class MyModel extends Serializable{
JSONObject obj;

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

JSONObject getJsonObject()
return this.obj;
} 
}

現在在onCreate和onSaveInstanceState方法的活動中執行以下操作。它看起來像這樣:

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

}

重新創建活動

在某些情況下,您的活動會因應用程序的正常行為而被銷毀,例如當用戶按下“返回”按鈕或您的活動通過調用finish()發出自己的破壞信號時。 如果系統當前已停止且未長時間使用或前台活動需要更多資源,系統也可能會破壞您的活動,因此系統必須關閉後台進程才能恢復內存。

當您的activity因用戶按下Back或activity自行完成而被銷毀時,系統對該Activity實例的概念將永遠消失,因為該行為表明不再需要該活動。 但是,如果系統因係統約束(而不是正常的應用程序行為)而破壞活動,那麼雖然實際的Activity實例已經消失,但係統會記住它存在,如果用戶導航回到它,系統會創建一個新的活動的實例,使用一組保存的數據來描述destroyed時的活動狀態。 系統用於恢復先前狀態的已保存數據稱為“實例狀態”,是存儲在Bundle對像中的鍵值對的集合。

要保存有關活動狀態的其他數據,必須覆蓋onSaveInstanceState()回調方法。 系統在用戶離開您的活動時調用此方法,並向其傳遞Bundle對象,該對象將在您的活動意外銷毀時保存。 如果系統必須稍後重新創建活動實例,它會將相同的Bundle對像傳遞給onRestoreInstanceState()onCreate()方法。

當系統開始停止活動時,它會調用onSaveInstanceState() (1),這樣您就可以指定要保存的其他狀態數據,以防必須重新創建Activity實例。 如果活動被銷毀並且必須重新創建相同的實例,則係統會將(1)中定義的狀態數據傳遞給onCreate()方法(2)和onRestoreInstanceState()方法(3)。

保存您的Activity狀態

當您的活動開始停止時,系統會調用onSaveInstanceState()以便您的活動可以使用一組鍵值對來保存狀態信息。 此方法的默認實現保存有關活動視圖層次結構狀態的信息,例如EditText小部件中的文本或ListView的滾動位置。

要保存活動的其他狀態信息,必須實現onSaveInstanceState()並將鍵值對添加到Bundle對象。 例如:

  static final String STATE_SCORE = "playerScore";
  static final String STATE_LEVEL = "playerLevel";

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

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

警告:始終調用onSaveInstanceState()的超類實現,以便默認實現可以保存視圖層次結構的狀態。

恢復您的Activity狀態

在先前銷毀活動之後重新創建活動時,您可以從系統傳遞活動的Bundle中恢復已保存的狀態。 onCreate()onRestoreInstanceState()回調方法都接收包含實例狀態信息的相同Bundle

因為無論系統是創建活動的新實例還是重新創建前一個實例,都會調用onCreate()方法,因此在嘗試讀取之前必須檢查狀態Bundle是否為null。 如果它為null,則係統正在創建活動的新實例,而不是恢復已銷毀的先前實例。

例如,以下是如何在onCreate()恢復某些狀態數據:

 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState); // Always call the superclass first

 // Check whether we're recreating a previously destroyed instance
 if (savedInstanceState != null) {
    // Restore value of members from saved state
    mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
    mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
 } else {
    // Probably initialize members with default values for a new instance
 }

 }

您可以選擇實現onRestoreInstanceState() ,系統在onStart()方法之後調用onRestoreInstanceState()而不是在onCreate()期間恢復狀態。 僅當存在要恢復的已保存狀態時,系統才會調用onRestoreInstanceState() ,因此您無需檢查Bundle是否為null:

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

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

僅在旋轉屏幕(方向改變)時,onSaveInstanceState(bundle)onRestoreInstanceState(bundle)方法對於數據持久性是有用的。
他們不是在應用程序之間切換(因為即使是好的onSaveInstanceState()方法被調用,但onCreate(bundle)onRestoreInstanceState(bundle)沒有再次調用。
欲了解更多持久使用共享偏好。閱讀這篇文章


簡單快速解決這個問題就是使用Icepick

首先,設置庫 app/build.gradle

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

現在,讓我們查看下面的示例如何在Activity中保存狀態

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

它適用於活動,碎片或任何需要在Bundle上序列化其狀態的對象(例如迫擊砲的ViewPresenters)

Icepick還可以為自定義視圖生成實例狀態代碼:

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
}

就我而言,拯救國家充其量只是一個障礙。 如果需要保存持久數據,只需使用SQLite數據庫即可。 Android讓SOOO變得簡單。

像這樣的東西:

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;

Kotlin代碼:

保存:

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

然後在onCreate()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

如果您不想擁有Optionals,請添加默認值


以下是Steve Moseley的答案(由ToolmakerSteve撰寫)的評論,該評論將事物置於透視中(在整個onSaveInstanceState vs onPause,east cost vs west cost saga)

@VVK - 我部分不同意。退出應用程序的某些方法不會觸發onSaveInstanceState(oSIS)。這限制了oSIS的有用性。它值得支持,用於最小的操作系統資源,但如果應用程序想要將用戶返回到他們所處的狀態,無論應用程序如何退出,都必須使用持久存儲方法。我使用onCreate來檢查bundle,如果它丟失了,那麼檢查 持久存儲。這集中了決策。我可以從崩潰,後退按鈕退出或自定義菜單項退出恢復,或者在很多天后回到屏幕用戶。 - ToolmakerSteve 2015年9月19日10:38


當系統需要內存並殺死應用程序時,會調用onSaveInstanceState 。 用戶剛剛關閉應用程序時不會調用它。 所以我認為應用程序狀態也應該保存在onPause它應該保存到一些持久存儲器,如PreferencesSqlite


儘管接受的答案是正確的,但是使用名為Icepick的庫在Android上保存活動狀態的方法更快更容易。Icepick是一個註釋處理器,負責處理為您保存和恢復狀態時使用的所有樣板代碼。

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

這樣做是一樣的:

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將使用任何保存其狀態的對象Bundle


為了幫助澄清這種瘋狂,我想首先代表所有Android用戶道歉,谷歌對軟鍵盤的徹頭徹尾的荒謬處理。 對於同樣簡單的問題,有很多答案,每個不同的原因是因為這個API與Android中的許多其他答案一樣,設計非常糟糕。 我可以想到沒有禮貌的方式陳述它。

我想隱藏鍵盤。 我期望為Android提供以下聲明: Keyboard.hide() 。 結束。 非常感謝你。 但Android存在問題。 您必須使用InputMethodManager來隱藏鍵盤。 好的,很好,這是Android的鍵盤API。 但! 您需要具有Context才能訪問IMM。 現在我們遇到了問題。 我可能想要從沒有任何使用或不需要任何Context的靜態或實用程序類中隱藏鍵盤。 或者更糟糕的是,IMM要求您指定要隱藏鍵盤FROM的View (甚至更糟糕的是什麼Window )。

這使得隱藏鍵盤變得如此具有挑戰性。 親愛的谷歌:當我查找蛋糕的配方時,地球上沒有RecipeProvider拒絕向我提供配方,除非我第一次回答世界衛生組織,蛋糕將被吃掉,哪裡會被吃掉!

這個悲傷的故事以醜陋的事實告終:要隱藏Android鍵盤,您需要提供兩種形式的識別: Context以及ViewWindow

我已經創建了一個靜態實用程序方法,只要你從一個Activity調用它就可以非常穩定地完成工作。

public static void hideKeyboard(Activity activity) {
    InputMethodManager imm = (InputMethodManager) activity.getSystemService(Activity.INPUT_METHOD_SERVICE);
    //Find the currently focused view, so we can grab the correct window token from it.
    View view = activity.getCurrentFocus();
    //If no view currently has focus, create a new one, just so we can grab a window token from it
    if (view == null) {
        view = new View(activity);
    }
    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}

請注意,此實用程序方法僅在從Activity調用時才有效! 上面的方法調用目標Activity getCurrentFocus來獲取正確的窗口標記。

但是假設您想要從DialogFragment託管的EditText隱藏鍵盤? 你不能使用上面的方法:

hideKeyboard(getActivity()); //won't work

這將無效,因為您將傳遞對Fragment的主機Activity的引用,當Fragment ,它將沒有集中控制! 哇! 因此,為了隱藏鍵盤中的碎片,我採用較低級別,更常見,更醜陋:

public static void hideKeyboardFrom(Context context, View view) {
    InputMethodManager imm = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
    imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
}

以下是從更多時間浪費在追逐此解決方案中收集到的一些其他信息:

關於windowSoftInputMode

還有另一個爭論點需要注意。 默認情況下,Android會自動將初始焦點分配給Activity的第一個EditText或focusable控件。 由此可見,InputMethod(通常是軟鍵盤)將通過顯示自身來響應焦點事件。 AndroidManifest.xmlwindowSoftInputMode屬性設置為stateAlwaysHidden ,指示鍵盤忽略此自動分配的初始焦點。

<activity
    android:name=".MyActivity"
    android:windowSoftInputMode="stateAlwaysHidden"/>

幾乎令人難以置信的是,當您觸摸控件時,似乎無法阻止鍵盤打開(除非將focusable="false"和/或focusableInTouchMode="false"分配給控件)。 顯然,windowSoftInputMode設置僅適用於自動焦點事件,而不適用於觸發觸摸事件觸發的事件。

因此, stateAlwaysHidden名字確實非常糟糕。 它也許應該被稱為ignoreInitialFocus

希望這可以幫助。

更新:獲取窗口令牌的更多方法

如果沒有焦點視圖(例如,如果你剛剛更改了片段就會發生),還有其他視圖將提供有用的窗口令牌。

if (view == null) view = new View(activity);這些是上述代碼的替代品if (view == null) view = new View(activity); 這些並未明確提及您的活動。

在片段類中:

view = getView().getRootView().getWindowToken();

給定片段fragment作為參數:

view = fragment.getView().getRootView().getWindowToken();

從您的內容正文開始:

view = findViewById(android.R.id.content).getRootView().getWindowToken();

更新2:清除焦點以避免在從後台打開應用程序時再次顯示鍵盤

將此行添加到方法的末尾:

view.clearFocus();





android android-activity application-state