How to save an activity state using save instance

2020-01-22 11:01发布

I've been working on the Android SDK platform, and it is a little unclear how to save an application's state. So given this minor re-tooling of the 'Hello, Android' example:

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

I thought it would be enough for the simplest case, but it always responds with the first message, no matter how I navigate away from the app.

I'm sure the solution is as simple as overriding onPause or something like that, but I've been poking away in the documentation for 30 minutes or so and haven't found anything obvious.

30条回答
太酷不给撩
2楼-- · 2020-01-22 11:28

Both methods are useful and valid and both are best suited for different scenarios:

  1. The user terminates the application and re-opens it at a later date, but the application needs to reload data from the last session – this requires a persistent storage approach such as using SQLite.
  2. The user switches application and then comes back to the original and wants to pick up where they left off - save and restore bundle data (such as application state data) in onSaveInstanceState() and onRestoreInstanceState() is usually adequate.

If you save the state data in a persistent manner, it can be reloaded in an onResume() or onCreate() (or actually on any lifecycle call). This may or may not be desired behaviour. If you store it in a bundle in an InstanceState, then it is transient and is only suitable for storing data for use in the same user ‘session’ (I use the term session loosely) but not between ‘sessions’.

It is not that one approach is better than the other, like everything, it is just important to understand what behaviour you require and to select the most appropriate approach.

查看更多
Juvenile、少年°
3楼-- · 2020-01-22 11:29

Now Android provides ViewModels for saving state, you should try to use that instead of saveInstanceState.

查看更多
叼着烟拽天下
4楼-- · 2020-01-22 11:31

My colleague wrote an article explaining application state on Android devices including explanations on activity lifecycle and state information, how to store state information, and saving to state Bundle and SharedPreferences and take a look at here.

The article covers three approaches:

Store local variable/UI control data for application lifetime (i.e. temporarily) using an instance state bundle

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

Store local variable/UI control data between application instances (i.e. permanently) using shared preferences

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

Keeping object instances alive in memory between activities within application lifetime using a retained non-configuration instance

[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();
}
查看更多
再贱就再见
5楼-- · 2020-01-22 11:31

Kotlin

You must override onSaveInstanceState and onRestoreInstanceState to store and retrieve your variables you want to be persistent

Life cycle graph

Store 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")
}

Retrieve 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
}
查看更多
啃猪蹄的小仙女
6楼-- · 2020-01-22 11:33

This is a classic 'gotcha' of Android development. There are two issues here:

  • There is a subtle Android Framework bug which greatly complicates application stack management during development, at least on legacy versions (not entirely sure if/when/how it was fixed). I'll discuss this bug below.
  • The 'normal' or intended way to manage this issue is, itself, rather complicated with the duality of onPause/onResume and onSaveInstanceState/onRestoreInstanceState

Browsing across all these threads, I suspect that much of the time developers are talking about these two different issues simultaneously ... hence all the confusion and reports of "this doesn't work for me".

First, to clarify the 'intended' behavior: onSaveInstance and onRestoreInstance are fragile and only for transient state. The intended usage (afaict) is to handle Activity recreation when the phone is rotated (orientation change). In other words, the intended usage is when your Activity is still logically 'on top', but still must be reinstantiated by the system. The saved Bundle is not persisted outside of the process/memory/gc, so you cannot really rely on this if your activity goes to the background. Yes, perhaps your Activity's memory will survive its trip to the background and escape GC, but this is not reliable (nor is it predictable).

So if you have a scenario where there is meaningful 'user progress' or state that should be persisted between 'launches' of your application, the guidance is to use onPause and onResume. You must choose and prepare a persistent store yourself.

BUT - there is a very confusing bug which complicates all of this. Details are here:

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

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

Basically, if your application is launched with the SingleTask flag, and then later on you launch it from the home screen or launcher menu, then that subsequent invocation will create a NEW task ... you'll effectively have two different instances of your app inhabiting the same stack ... which gets very strange very fast. This seems to happen when you launch your app during development (i.e. from Eclipse or Intellij), so developers run into this a lot. But also through some of the app store update mechanisms (so it impacts your users as well).

I battled through these threads for hours before I realized that my main issue was this bug, not the intended framework behavior. A great writeup and workaround (UPDATE: see below) seems to be from user @kaciula in this answer:

Home key press behaviour

UPDATE June 2013: Months later, I have finally found the 'correct' solution. You don't need to manage any stateful startedApp flags yourself, you can detect this from the framework and bail appropriately. I use this near the beginning of my 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;
    }
}
查看更多
Bombasti
7楼-- · 2020-01-22 11:33

To answer the original question directly. savedInstancestate is null because your Activity is never being re-created.

Your Activity will only be re-created with a state bundle when:

  • Configuration changes such as changing the orientation or phone language which may requires a new activity instance to be created.
  • You return to the app from the background after the OS has destroyed the activity.

Android will destroy background activities when under memory pressure or after they've been in the background for an extended period of time.

When testing your hello world example there are a few ways to leave and return to the Activity.

  • When you press the back button the Activity is finished. Re-launching the app is a brand new instance. You aren't resuming from the background at all.
  • When you press the home button or use the task switcher the Activity will go into the background. When navigating back to the application onCreate will only be called if the Activity had to be destroyed.

In most cases if you're just pressing home and then launching the app again the activity won't need to be re-created. It already exists in memory so onCreate() won't be called.

There is an option under Settings -> Developer Options called "Don't keep activities". When it's enabled Android will always destroy activities and recreate them when they're backgrounded. This is a great option to leave enabled when developing because it simulates the worst case scenario. ( A low memory device recycling your activities all the time ).

The other answers are valuable in that they teach you the correct ways to store state but I didn't feel they really answered WHY your code wasn't working in the way you expected.

查看更多
登录 后发表回答