How can I save an activity state using the save instance state?

Viewed 879845

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.

35 Answers

You need to override onSaveInstanceState(Bundle savedInstanceState) and write the application state values you want to change to the Bundle parameter like this:

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

The Bundle is essentially a way of storing a NVP ("Name-Value Pair") map, and it will get passed in to onCreate() and also onRestoreInstanceState() where you would then extract the values from activity like this:

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

Or from a fragment.

@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
    super.onViewStateRestored(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");
}

You would usually use this technique to store instance values for your application (selections, unsaved text, etc.).

The savedInstanceState is only for saving state associated with a current instance of an Activity, for example current navigation or selection info, so that if Android destroys and recreates an Activity, it can come back as it was before. See the documentation for onCreate and onSaveInstanceState

For more long lived state, consider using a SQLite database, a file, or preferences. See Saving Persistent State.

Meanwhile I do in general no more use

Bundle savedInstanceState & Co

The life cycle is for most activities too complicated and not necessary.

And Google states itself, it is NOT even reliable.

My way is to save any changes immediately in the preferences:

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

In some way SharedPreferences work similar like Bundles. And naturally and at first such values have to be read from preferences.

In the case of complex data you may use SQLite instead of using preferences.

When applying this concept, the activity just continues to use the last saved state, regardless of whether it was an initial open with reboots in between or a reopen due to the back stack.

To get activity state data stored in onCreate(), first you have to save data in savedInstanceState by overriding SaveInstanceState(Bundle savedInstanceState) method.

When activity destroy SaveInstanceState(Bundle savedInstanceState) method gets called and there you save data you want to save. And you get same in onCreate() when activity restart.(savedInstanceState wont be null since you have saved some data in it before activity get destroyed)

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
}

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

There is a way to make Android save the states without implementing any method. Just add this line to your Manifest in Activity declaration:

android:configChanges="orientation|screenSize"

It should look like this:

<activity
    android:name=".activities.MyActivity"
    android:configChanges="orientation|screenSize">
</activity>

Here you can find more information about this property.

It's recommended to let Android handle this for you than the manually handling.

What to save and what not to?

Ever wondered why the text in the EditText gets saved automatically while an orientation change? Well, this answer is for you.

When an instance of an Activity gets destroyed and the System recreates a new instance (for example, configuration change). It tries to recreate it using a set of saved data of old Activity State (instance state).

Instance state is a collection of key-value pairs stored in a Bundle object.

By default System saves the View objects in the Bundle for example.

  • Text in EditText
  • Scroll position in a ListView, etc.

If you need another variable to be saved as a part of instance state you should OVERRIDE onSavedInstanceState(Bundle savedinstaneState) method.

For example, int currentScore in a GameActivity

More detail about the onSavedInstanceState(Bundle savedinstaneState) while saving data

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

So by mistake if you forget to call super.onSaveInstanceState(savedInstanceState);the default behavior will not work ie Text in EditText will not save.

Which to choose for restoring Activity state?

 onCreate(Bundle savedInstanceState)

OR

onRestoreInstanceState(Bundle savedInstanceState)

Both methods get the same Bundle object, so it does not really matter where you write your restoring logic. The only difference is that in onCreate(Bundle savedInstanceState) method you will have to give a null check while it is not needed in the latter case. Other answers have already code snippets. You can refer them.

More detail about the onRestoreInstanceState(Bundle savedinstaneState)

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

Always call super.onRestoreInstanceState(savedInstanceState); so that System restore the View hierarchy by default

Bonus

The onSaveInstanceState(Bundle savedInstanceState) is invoked by the system only when the user intends to come back to the Activity. For example, you are using App X and suddenly you get a call. You move to the caller app and come back to the app X. In this case the onSaveInstanceState(Bundle savedInstanceState) method will be invoked.

But consider this if a user presses the back button. It is assumed that the user does not intend to come back to the Activity, hence in this case onSaveInstanceState(Bundle savedInstanceState) will not be invoked by the system. Point being you should consider all the scenarios while saving data.

Relevant links:

Demo on default behavior
Android Official Documentation.

Instead of that, you should use ViewModel, which will retain the data until the activity life cycle.

Now it makes sense to do 2 ways in the view model. if you want to save the first as a saved instance: You can add state parameter in view model like this https://developer.android.com/topic/libraries/architecture/viewmodel-savedstate#java

or you can save variables or object in view model, in this case the view model will hold the life cycle until the activity is destroyed.

public class HelloAndroidViewModel extends ViewModel {
   public Booelan firstInit = false;

    public HelloAndroidViewModel() {
        firstInit = false;
    }
    ...
}

public class HelloAndroid extends Activity {

  private TextView mTextView = null;
  HelloAndroidViewModel viewModel = ViewModelProviders.of(this).get(HelloAndroidViewModel.class);
  /** Called when the activity is first created. */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mTextView = new TextView(this);

    //Because even if the state is deleted, the data in the viewmodel will be kept because the activity does not destroy
    if(!viewModel.firstInit){
        viewModel.firstInit = true
        mTextView.setText("Welcome to HelloAndroid!");
    }else{
       mTextView.setText("Welcome back.");
    }

    setContentView(mTextView);
  }
}

Kotlin Solution: For custom class save in onSaveInstanceState you can be converted your class to JSON string and restore it with Gson convertion and for single String, Double, Int, Long value save and restore as following. The following example is for Fragment and Activity:

For Activity:

For put data in saveInstanceState:

override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)

        //for custom class-----
        val gson = Gson()
        val json = gson.toJson(your_custom_class)
        outState.putString("CUSTOM_CLASS", json)

        //for single value------
        outState.putString("MyString", stringValue)
        outState.putBoolean("MyBoolean", true)
        outState.putDouble("myDouble", doubleValue)
        outState.putInt("MyInt", intValue)
    }

Restore data:

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

    //for custom class restore
    val json = savedInstanceState?.getString("CUSTOM_CLASS")
    if (!json!!.isEmpty()) {
        val gson = Gson()
        testBundle = gson.fromJson(json, Session::class.java)
    }

  //for single value restore

   val myBoolean: Boolean = savedInstanceState?.getBoolean("MyBoolean")
   val myDouble: Double = savedInstanceState?.getDouble("myDouble")
   val myInt: Int = savedInstanceState?.getInt("MyInt")
   val myString: String = savedInstanceState?.getString("MyString")
 }

You can restore it on Activity onCreate also.

For fragment:

For put class in saveInstanceState:

 override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        val gson = Gson()
        val json = gson.toJson(customClass)
        outState.putString("CUSTOM_CLASS", json)
    }

Restore data:

 override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        //for custom class restore
        if (savedInstanceState != null) {
            val json = savedInstanceState.getString("CUSTOM_CLASS")
            if (!json!!.isEmpty()) {
                val gson = Gson()
                val customClass: CustomClass = gson.fromJson(json, CustomClass::class.java)
            }
        }

      // for single value restore
      val myBoolean: Boolean = savedInstanceState.getBoolean("MyBoolean")
      val myDouble: Double = savedInstanceState.getDouble("myDouble")
      val myInt: Int = savedInstanceState.getInt("MyInt")
      val myString: String = savedInstanceState.getString("MyString")
    }

using Android ViewModel & SavedStateHandle to persist the serializable data

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
        binding.setViewModel(new ViewModelProvider(this).get(ViewModel.class));
        binding.setLifecycleOwner(this);
        setContentView(binding.getRoot());
    }

    public static class ViewModel extends AndroidViewModel {

        //This field SURVIVE the background process reclaim/killing & the configuration change
        public final SavedStateHandle savedStateHandle;

        //This field NOT SURVIVE the background process reclaim/killing but SURVIVE the configuration change
        public final MutableLiveData<String> inputText2 = new MutableLiveData<>();


        public ViewModel(@NonNull Application application, SavedStateHandle savedStateHandle) {
            super(application);
            this.savedStateHandle = savedStateHandle;
        }
    }
}

in layout file

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="viewModel"
            type="com.xxx.viewmodelsavedstatetest.MainActivity.ViewModel" />
    </data>

    <LinearLayout xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">


        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:autofillHints=""
            android:hint="This field SURVIVE the background process reclaim/killing &amp; the configuration change"
            android:text='@={(String)viewModel.savedStateHandle.getLiveData("activity_main/inputText", "")}' />

        <SeekBar
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:max="100"
            android:progress='@={(Integer)viewModel.savedStateHandle.getLiveData("activity_main/progress", 50)}' />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="This field SURVIVE the background process reclaim/killing &amp; the configuration change"
            android:text='@={(String)viewModel.savedStateHandle.getLiveData("activity_main/inputText", "")}' />

        <SeekBar
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:max="100"
            android:progress='@={(Integer)viewModel.savedStateHandle.getLiveData("activity_main/progress", 50)}' />

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="This field NOT SURVIVE the background process reclaim/killing but SURVIVE the configuration change"
            android:text='@={viewModel.inputText2}' />

    </LinearLayout>
</layout>

Test:

1. start the test activity
2. press home key to go home
3. adb shell kill <the test activity process>
4. open recent app list and restart the test activity

In 2020 we have some changes :

If you want your Activity to restore its state after the process is killed and started again, you may want to use the “saved state” functionality. Previously, you needed to override two methods in the Activity: onSaveInstanceState and onRestoreInstanceState. You can also access the restored state in the onCreate method. Similarly, in Fragment, you have onSaveInstanceState method available (and the restored state is available in the onCreate, onCreateView, and onActivityCreated methods).

Starting with AndroidX SavedState 1.0.0, which is the dependency of the AndroidX Activity and the AndroidX Fragment, you get access to the SavedStateRegistry. You can obtain the SavedStateRegistry from the Activity/Fragment and then register your SavedStateProvider :

class MyActivity : AppCompatActivity() {

  companion object {
    private const val MY_SAVED_STATE_KEY = "MY_SAVED_STATE_KEY "
    private const val SOME_VALUE_KEY = "SOME_VALUE_KEY "
  }
    
  private lateinit var someValue: String
  private val savedStateProvider = SavedStateRegistry.SavedStateProvider {    
    Bundle().apply {
      putString(SOME_VALUE_KEY, someValue)
    }
  }
  
  override fun onCreate(savedInstanceState: Bundle?) {    
    super.onCreate(savedInstanceState)
    savedStateRegistry.registerSavedStateProvider(MY_SAVED_STATE_KEY, savedStateProvider)
    someValue = savedStateRegistry.consumeRestoredStateForKey(MY_SAVED_STATE_KEY)?.getString(SOME_VALUE_KEY) ?: ""
  }
  
}

As you can see, SavedStateRegistry enforces you to use the key for your data. This can prevent your data from being corrupted by another SavedStateProvider attached to the same Activity/Fragment.Also you can extract your SavedStateProvider to another class to make it work with your data by using whatever abstraction you want and in such a way achieve the clean saved state behavior in your application.

Related