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.xml
file (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.xml
file in theapp
|src
|main
|res
|values
folder and add the dimensions you'll need for the app (you can do this by right-clicking on theres
|values
folder within Android Studio and selectingNew
values
):<?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_size
anddiscount_code_text_size
are 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 style
or 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 aTextView
with theEnter your name and email for a discount code at Your Favorite Restaurant!
text. This is done by adding theandroid:text
attribute with the@string/header_text
value:<?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
ConstraintLayout
for 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:textSize
attribute 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:padding
inConstraintLayout
sets 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_margin
specifies the margin value for all four sides of a View andandroid:layoutMarginTop
,android:layoutMarginBottom
,android:layoutMarginStart
, andandroid:layoutMarginEnd
allow 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:layoutMarginLeft
if you useandroid:layoutMarginStart
andandroid:layoutMarginRight
if 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.xml
file.To position the content within a View, you can specify
android:gravity
. Thecenter
value constrains the content both vertically and horizontally within the View. - Next, add three
EditText
views below theheader_text
for 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
EditText
fields have aninputType
attribute to specify the type of input that can be entered into the form field. Some values, such asnumber
onEditText
, 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:text
attribute to show the text that will typically be displayed in the form fields. This helps when you switch to theDesign
view from viewing the XML in theCode
view 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:
- Enter some text into each of the form fields:
- Now, use the second rotate button in the virtual device controls () to rotate the phone 90 degrees to the right:
Can you spot what has happened? The
Last Name
field value is no longer set. It has been lost in the process of recreating the activity. Why is this? Well, in the case of theEditText
fields, the Android framework will preserve the state of the fields if they have an ID set on them. - Go back to the
activity_main.xml
layout file and add an ID for theLast Name
value in theEditText
field:<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.kt
and 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_name
andlast_name
fields, concatenate them with a space, and then use a string resource to format the discount code confirmation text. The string you reference in thestrings.xml
file is as follows:<string name="discount_code_confirmation">Hey %s! Here is your discount code</string>
The
%s
value 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.util
package. 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
: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:
Oh, no! The discount code has gone. The
TextView
fields do not retain the state, so you will have to save the state yourself. - Go back into
MainActivity.kt
and 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
MainActivity
companion 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
EditText
fields, and then generate a discount code. Then, rotate the device and you will see that the discount code is restored in Figure 2.11:
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.