Saving and Restoring the Activity State
In this section, you'll explore how your Activity saves and restores the state. As you've learned in the previous section, configuration changes, such as rotating the phone, cause the Activity to be recreated. This can also happen if the system has to kill your app in order to free up memory. In these scenarios, it is important to preserve the state of the Activity and then restore it. In the next two exercises, you'll work through an example ensuring that the user's data is restored when TextView is created and populated from a user's data after filling in a form.
Exercise 2.02: Saving and Restoring the State in Layouts
In this exercise, firstly create an application called Save and Restore with an empty activity. The app you are going to create will have a simple form that offers a discount code for a user's favorite restaurant if they enter some personal details (no actual information will be sent anywhere, so your data is safe):
- Open up the
strings.xmlfile (located inapp|src|main|res|values|strings.xml) and create the following strings that you'll need for your app:<resources> <string name="app_name">Save And Restore</string> <string name="header_text">Enter your name and email for a discount code at Your Favorite Restaurant! </string> <string name="first_name_label">First Name:</string> <string name="email_label">Email:</string> <string name="last_name_label">Last Name:</string> <string name="discount_code_button">GET DISCOUNT</string> <string name="discount_code_confirmation">Your discount code is below %s. Enjoy!</string> </resources>
- You are also going to specify some text sizes, layout margins, and padding directly, so create the
dimens.xmlfile in theapp|src|main|res|valuesfolder and add the dimensions you'll need for the app (you can do this by right-clicking on theres|valuesfolder within Android Studio and selectingNewvalues):<?xml version="1.0" encoding="utf-8"?> <resources> <dimen name="grid_4">4dp</dimen> <dimen name="grid_8">8dp</dimen> <dimen name="grid_12">12dp</dimen> <dimen name="grid_16">16dp</dimen> <dimen name="grid_24">24dp</dimen> <dimen name="grid_32">32dp</dimen> <dimen name="default_text_size">20sp</dimen> <dimen name="discount_code_text_size">20sp</dimen> </resources>
Here, you are specifying all the dimensions you need in the exercise. You will see here that
default_text_sizeanddiscount_code_text_sizeare specified insp. They represent the same values as density-independent pixels, which not only define the size measurement according to the density of the device that your app is being run on but also change the text size according to the user's preference, defined inSettings|Display|Font style(this might beFont size and styleor something similar, depending on the exact device you are using). - In
R.layout.activity_main, add the following XML, creating a containing layout file and adding header aTextViewwith theEnter your name and email for a discount code at Your Favorite Restaurant!text. This is done by adding theandroid:textattribute with the@string/header_textvalue:<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="@dimen/grid_4" android:layout_marginTop="@dimen/grid_4" tools:context=".MainActivity"> <TextView android:id="@+id/header_text" android:gravity="center" android:textSize="@dimen/default_text_size" android:paddingStart="@dimen/grid_8" android:paddingEnd="@dimen/grid_8" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/header_text" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"/> </androidx.constraintlayout.widget.ConstraintLayout>
You are using
ConstraintLayoutfor constraining Views against the parent View and sibling Views.Although you should normally specify the display of the View with styles, you can do this directly in the XML, as is done for some attributes here. The value of the
android:textSizeattribute is@dimen/default_text_size, defined in the previous code block, which you use to avoid repetition, and it enables you to change all the text size in one place. Using styles is the preferred option for setting text sizes as you will get sensible defaults and you can override the value in the style or, as you are doing here, on the individual Views.Other attributes that affect positioning are also specified directly here in the Views. The most common ones are padding and margin. Padding is applied on the inside of Views and is the space between the text and the border. Margin is specified on the outside of Views and is the space from the outer edges of Views. For example,
android:paddinginConstraintLayoutsets the padding for the View with the specified value on all sides. Alternatively, you can specify the padding for one of the four sides of a View withandroid:paddingTop,android:paddingBottom,android:paddingStart, andandroid:paddingEnd. This pattern also exists to specify margins, soandroid:layout_marginspecifies the margin value for all four sides of a View andandroid:layoutMarginTop,android:layoutMarginBottom,android:layoutMarginStart, andandroid:layoutMarginEndallow setting the margin for individual sides.For API levels less than 17 (and your app supports down to 16) you also have to add
android:layoutMarginLeftif you useandroid:layoutMarginStartandandroid:layoutMarginRightif you useandroid:layoutMarginEnd. In order to have consistency and uniformity throughout the app, you define the margin and padding values as dimensions contained within thedimens.xmlfile.To position the content within a View, you can specify
android:gravity. Thecentervalue constrains the content both vertically and horizontally within the View. - Next, add three
EditTextviews below theheader_textfor the user to add their first name, last name, and email:<EditText android:id="@+id/first_name" android:textSize="@dimen/default_text_size" android:layout_marginStart="@dimen/grid_24" android:layout_marginLeft="@dimen/grid_24" android:layout_marginEnd="@dimen/grid_16" android:layout_marginRight="@dimen/grid_16" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="@string/first_name_label" android:inputType="text" app:layout_constraintTop_toBottomOf="@id/header_text" app:layout_constraintStart_toStartOf="parent" /> <EditText android:textSize="@dimen/default_text_size" android:layout_marginEnd="@dimen/grid_24" android:layout_marginRight="@dimen/grid_24" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="@string/last_name_label" android:inputType="text" app:layout_constraintTop_toBottomOf="@id/header_text" app:layout_constraintStart_toEndOf="@id/first_name" app:layout_constraintEnd_toEndOf="parent" /> <!-- android:inputType="textEmailAddress" is not enforced, but is a hint to the IME (Input Method Editor) usually a keyboard to configure the display for an email - typically by showing the '@' symbol --> <EditText android:id="@+id/email" android:textSize="@dimen/default_text_size" android:layout_marginStart="@dimen/grid_24" android:layout_marginLeft="@dimen/grid_24" android:layout_marginEnd="@dimen/grid_32" android:layout_marginRight="@dimen/grid_32" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/email_label" android:inputType="textEmailAddress" app:layout_constraintTop_toBottomOf="@id/first_name" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" />
The
EditTextfields have aninputTypeattribute to specify the type of input that can be entered into the form field. Some values, such asnumberonEditText, restrict the input that can be entered into the field, and on selecting the field, suggest how the keyboard is displayed. Others, such asandroid:inputType="textEmailAddress", will not enforce an@symbol being added to the form field, but will give a hint to the keyboard to display it. - Finally, add a button for the user to press to generate a discount code, and display the discount code itself and a confirmation message:
<Button android:id="@+id/discount_button" android:textSize="@dimen/default_text_size" android:layout_marginTop="@dimen/grid_12" android:gravity="center" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/discount_code_button" app:layout_constraintTop_toBottomOf="@id/email" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"/> <TextView android:id="@+id/discount_code_confirmation" android:gravity="center" android:textSize="@dimen/default_text_size" android:paddingStart="@dimen/grid_16" android:paddingEnd="@dimen/grid_16" android:layout_marginTop="@dimen/grid_8" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/discount_button" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" tools:text="Hey John Smith! Here is your discount code" /> <TextView android:id="@+id/discount_code" android:gravity="center" android:textSize="@dimen/discount_code_text_size" android:textStyle="bold" android:layout_marginTop="@dimen/grid_8" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@id/discount_code _confirmation" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" tools:text="XHFG6H9O" />
There are also some attributes that you haven't seen before. The tools namespace
xmlns:tools="http://schemas.android.com/tools"which was specified at the top of the xml layout file enables certain features that can be used when creating your app to assist with configuration and design. The attributes are removed when you build your app, so they don't contribute to the overall size of the app. You are using thetools:textattribute to show the text that will typically be displayed in the form fields. This helps when you switch to theDesignview from viewing the XML in theCodeview in Android Studio as you can see an approximation of how your layout displays on a device. - Run the app and you should see the output displayed in Figure 2.6:

Figure 2.6: The Activity screen on the first launch
- Enter some text into each of the form fields:

Figure 2.7: The EditText fields filled in
- Now, use the second rotate button in the virtual device controls (
) to rotate the phone 90 degrees to the right:
Figure 2.8: The virtual device turned to landscape orientation
Can you spot what has happened? The
Last Namefield value is no longer set. It has been lost in the process of recreating the activity. Why is this? Well, in the case of theEditTextfields, the Android framework will preserve the state of the fields if they have an ID set on them. - Go back to the
activity_main.xmllayout file and add an ID for theLast Namevalue in theEditTextfield:<EditText android:id="@+id/last_name" android:textSize="@dimen/default_text_size" android:layout_marginEnd="@dimen/grid_24" android:layout_width="wrap_content" android:layout_height="wrap_content" android:hint="@string/last_name_label" android:inputType="text" app:layout_constraintTop_toBottomOf="@id/header_text" app:layout_constraintStart_toEndOf="@id/first_name" app:layout_constraintEnd_toEndOf="parent" tools:text="Last Name:"/>
When you run up the app again and rotate the device, it will preserve the value you have entered. You've now seen that you need to set an ID on the EditText fields to preserve the state. For the EditText fields, it's common to retain the state on a configuration change when the user is entering details into a form so that it is the default behavior if the field has an ID. Obviously, you want to get the details of the EditText field once the user has entered some text, which is why you set an ID, but setting an ID for other field types, such as TextView, does not retain the state if you update them and you need to save the state yourself. Setting IDs for Views that enable scrolling, such as RecyclerView, is also important as it enables the scroll position to be maintained when the Activity is recreated.
Now, you have defined the layout for the screen, but you have not added any logic for creating and displaying the discount code. In the next exercise, we will work through this.
The layout created in this exercise is available at http://packt.live/35RSdgz.
You can find the code for the entire exercise at http://packt.live/3p1AZF3.
Exercise 2.03: Saving and Restoring the State with Callbacks
The aim of this exercise is to bring all the UI elements in the layout together to generate a discount code after the user has entered their data. In order to do this, you will have to add logic to the button to retrieve all the EditText fields and then display a confirmation to the user, as well as generate a discount code:
- Open up
MainActivity.ktand replace the default empty Activity from the project creation. A snippet of the code is shown here, but you'll need to use the link given below to find the full code block you need to add:MainActivity.kt
14 class MainActivity : AppCompatActivity() { 15 16 private val discountButton: Button 17 get() = findViewById(R.id.discount_button) 18 19 private val firstName: EditText 20 get() = findViewById(R.id.first_name) 21 22 private val lastName: EditText 23 get() = findViewById(R.id.last_name) 24 25 private val email: EditText 26 get() = findViewById(R.id.email) 27 28 private val discountCodeConfirmation: TextView 29 get() = findViewById(R.id .discount_code_confirmation) 30 31 private val discountCode: TextView 32 get() = findViewById(R.id.discount_code) 33 34 override fun onCreate(savedInstanceState: Bundle?) { 35 super.onCreate(savedInstanceState) 36 setContentView(R.layout.activity_main) 37 Log.d(TAG, "onCreate")You can find the complete code here http://packt.live/38XcdQS.
The
get() = …is a custom accessor for a property.Upon clicking the discount button, you retrieve the values from the
first_nameandlast_namefields, concatenate them with a space, and then use a string resource to format the discount code confirmation text. The string you reference in thestrings.xmlfile is as follows:<string name="discount_code_confirmation">Hey %s! Here is your discount code</string>
The
%svalue specifies a string value to be replaced when the string resource is retrieved. This is done by passing in the full name when getting the string:getString(R.string.discount_code_confirmation, fullName)
The code is generated by using the UUID (Universally Unique Identifier) library from the
java.utilpackage. This creates a unique id, and then thetake()Kotlin function is used to get the first eight characters before setting these to uppercase. Finally, discount_code is set in the view, the keyboard is hidden, and all the form fields are set back to their initial values. - Run the app and enter some text into the name and email fields, and then click on
GET DISCOUNT:
Figure 2.9: Screen displayed after the user has generated a discount code
The app behaves as expected, showing the confirmation.
- Now, rotate the phone (pressing the fifth button down with the arrow on the right-hand side of the virtual device picture) and observe the result:

Figure 2.10: Discount code no longer displaying on the screen
Oh, no! The discount code has gone. The
TextViewfields do not retain the state, so you will have to save the state yourself. - Go back into
MainActivity.ktand add the following Activity callbacks:override fun onRestoreInstanceState(savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) Log.d(TAG, "onRestoreInstanceState") } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) Log.d(TAG, "onSaveInstanceState") }These callbacks, as the names declare, enable you to save and restore the instance state.
onSaveInstanceState(outState: Bundle)allows you to add key-value pairs from your Activity when it is being backgrounded or destroyed, which you can retrieve in eitheronCreate(savedInstanceState: Bundle?)oronRestoreInstanceState(savedInstanceState: Bundle).So, you have two callbacks to retrieve the state once it has been set. If you are doing a lot of initialization in
onCreate(savedInstanceState: Bundle), it might be better to useonRestoreInstanceState(savedInstanceState: Bundle)to retrieve this instance state when your Activity is being recreated. In this way, it's clear which state is being recreated. However, you might prefer to useonCreate(savedInstanceState: Bundle)if there is minimal setup required.Whichever of the two callbacks you decide to use, you will have to get the state you set in the
onSaveInstanceState(outState: Bundle)call. For the next step in the exercise, you will useonRestoreInstanceState(savedInstanceState: Bundle). - Add two constants to the
MainActivitycompanion object:private const val DISCOUNT_CONFIRMATION_MESSAGE = "DISCOUNT_CONFIRMATION_MESSAGE" private const val DISCOUNT_CODE = "DISCOUNT_CODE"
- Now, add these constants as keys for the values you want to save and retrieve by making the following additions to the Activity:
override fun onRestoreInstanceState( savedInstanceState: Bundle) { super.onRestoreInstanceState(savedInstanceState) Log.d(TAG, "onRestoreInstanceState") //Get the discount code or an empty string if it hasn't been set discountCode.text = savedInstanceState .getString(DISCOUNT_CODE,"") //Get the discount confirmation message or an empty string if it hasn't been set discountCodeConfirmation.text = savedInstanceState.getString( DISCOUNT_CONFIRMATION_MESSAGE,"") } override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) Log.d(TAG, "onSaveInstanceState") outState.putString(DISCOUNT_CODE, discountCode.text.toString()) outState.putString(DISCOUNT_CONFIRMATION_MESSAGE, discountCodeConfirmation.text.toString()) } - Run the app, enter the values into the
EditTextfields, and then generate a discount code. Then, rotate the device and you will see that the discount code is restored in Figure 2.11:
Figure 2.11: Discount code continues to be displayed on the screen
In this exercise, you first saw how the state of the EditText fields is maintained on configuration changes. You also saved and restored the instance state using the Activity lifecycle onSaveInstanceState(outState: Bundle) and onCreate(savedInstanceState: Bundle?)/onRestoreInstanceState(savedInstanceState: Bundle) functions. These functions provide a way to save and restore simple data. The Android framework also provides ViewModel, an Android architecture component that is lifecycle-aware. The mechanisms of how to save and restore this state (with ViewModel) are managed by the framework, so you don't have to explicitly manage it as you have done in the preceding example. You will learn how to use this component in Chapter 10, Android Architecture Components.
So far, you have created a single-screen app. Although it is possible for simple apps to use one Activity, it is likely that you will want to organize your app into different activities that handle different functions. So, in the next section, you will add another Activity to an app and navigate between the activities.