Android Lifecycle

Understanding the Android lifecycle and responding correctly to state changes is crucial to building apps with fewer bugs that use fewer resources and provide a good user experience. By Denis Buketa.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Saving and Restoring the Activity Instance State

If you’ve played a bit with the app, you might’ve noticed a couple bugs. Increase the counters by tapping the cards on the Main screen.

Main screen in portrait orientation with counters showing values that are not zero

Now, rotate the device to change the screen orientation. If your device’s display has auto-rotate enabled, you’ll see something like this:

Main screen in landscape orientation with counters showing values that are all zeros

The counter’s state got lost during the screen orientation change. Inspect the logs:

Logs showing activity lifecycle changes during screen orientation change

You can see that when the screen orientation change started, the app destroyed portrait activity, then created and resumed the new landscape activity. Since in MainActivity.kt you don’t have any logic for saving and restoring the counter state, it was lost during this process.

You’ll fix that next! :]

Saving Instance State

Open MainActivity.kt and add the following code:

override fun onSaveInstanceState(outState: Bundle) {
  Timber.i("PuppyCounter - MainActivity - onSaveInstanceState()")

  // Save the dog count state
  outState.putParcelable(STATE_DOG_COUNT, dogCount)

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

As an activity begins to stop, the OS calls onSaveInstanceState() for the activity to save any state information to an instance state bundle. Some Android views handle that by default — EditText for the text and ListView for the scroll position.

Note: onSaveInstanceState() isn’t called when the user explicitly closes the activity or in cases when finish() is called.

To save dogCount‘s state, you’ve overridden onSaveInstanceState() and you stored the state to Bundle as a key-value pair using outState.putParcelable(). For the key, you used STATE_DOG_COUNT, which was already defined in the class.

Check DogCount class. You’ll notice that it implements Parcelable. In case you’re not familiar with Parcelable, it’s an interface conceptually similar to Java Serializable. Classes that implement Parcelable can be written to and restored from a Parcel, which is designed for high-performance IPC transport. To put it simply, it allows you to store simple data structures in Bundle.

Restoring Instance State

Good! You now have the logic for storing the state, but that isn’t so useful unless you have the logic for retrieving it. In MainActivity.kt, add the following code below onSaveInstanceState():

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
  Timber.i("PuppyCounter - MainActivity - onRestoreInstanceState()")

  // Always call the superclass so it can restore the view hierarchy
  super.onRestoreInstanceState(savedInstanceState)

  dogCount = savedInstanceState.getParcelable(STATE_DOG_COUNT) ?: DogCount()
}

Any state that you save in onSaveInstanceState(), you can restore in onRestoreInstanceState(). onRestoreInstanceState() receives Bundle that contains key-value pairs that you can read. Here, you used savedInstanceState.getParcelable() to retrieve DogCount‘s state. Notice that you used the same key that you used for saving the state: STATE_DOG_COUNT.

Note: The OS invokes onRestoreInstanceState() after onStart() callback only if it has a saved state to restore. You can also restore the state in onCreate() because that callback receives the same Bundle.

Build and run your app. Increase the counters and change the orientation:

Main screen in landscape orientation where counters are showing values that were present in portrait orientation as well

Also check the logs if you’re interested in when each callback invocation occurred:

Logs showing activity lifecycle with additional callbacks when saving and restoring the instance state

Note: Don’t confuse onSaveInstanceState() and onRestoreInstanceState() with activity lifecycle callbacks. The OS invokes these methods only if there’s a case for it.

Excellent! Now that you’ve fixed one bug in the app, it’s time to move to the next one. :]

Passing Data Between Screens

Increase the counters in the Main screen and then open the Share screen. You’ll notice that the Share screen’s values don’t match those in the Main screen.

Main screen and Share screen where it can be seen that values shown in counters on Main screen are not passed to the Share screen

In MainActivity.kt, modify showShareScreen() like this:

private fun showShareScreen() {
  Timber.i("PuppyCounter - MainActivity - start ShareActivity Intent")
  val intent = ShareActivity.createIntent(this)
  
  // Store DogCount state to the intent
  intent.putExtra(ShareActivity.EXTRA_DOG_COUNT, dogCount)
  
  startActivity(intent)
}

With this code, you store DogCount‘s state in Intent. Here, you’re using an approach similar to what you saw in the previous section. OK, this will pass the data to the ShareActivity, but you still have to add the logic to retrieve it.

In ShareActivity.kt, add the following method:

private fun readExtras() = intent.extras?.run {
  Timber.i("PuppyCounter - ShareActivity - readExtras()")
  dogCount = getParcelable(EXTRA_DOG_COUNT) ?: DogCount()
}

This method retrieves the Intent object that started this activity and tries to get the extra data that was passed with it. In this particular case, it’ll try to retrieve DogCount‘s state.

To complete the retrieving logic, invoke this method in onCreate() in ShareActivity.kt:

override fun onCreate(savedInstanceState: Bundle?) {
  Timber.i("PuppyCounter - ShareActivity - onCreate()")
  super.onCreate(savedInstanceState)
  setContentView(R.layout.layout_share)
  findViews()

  // Read extra data from the Intent
  readExtras()
  
  setOnShareBtnClickListener()
}

When retrieving the data from the intent, the best place to do it is in onCreate(). That way, you have the time to set up the state before the activity resumes and the user starts interacting with it.

Great! Build and run your app. Increase the counters and then open the Share screen. You’ll see something like this:

Main screen and Share screen where it can be seen that values shown in counters on Main screen are passed to the Share screen

Check the logs to see the lifecycles of both activities as you move from one screen to another.

Logs showing activity lifecycle when moving from one activity to another

Notice how the OS creates ShareActivity just after MainActivity‘s onPause() call. As mentioned before, the app calls onStop() when activity is no longer visible to the user. After MainActivity‘s onPause() call, you can see the series of ShareActivity‘s lifecycle callbacks that include reading intent data. When resumed, ShareActivity is completely visible to the user and MainActivity‘s onStop() can be called, followed by onSaveInstanceState().

Understanding Intent Data

Change the screen orientation on the Share screen and notice what happens. You’ll see that the app preserved dogCount‘s state. How’s that possible if you haven’t implemented the logic to save and retrieve the instance state?

Check the logs! :]

Logs showing ShareActivity lifecycle on the screen orientation change with additional log when reading extras

You’re already familiar with how the state can be lost during a configuration change. In this case, notice how the readExtras() log is again present when the app creates the new ShareActivity. But if you check the code, you see that you print that log only if intent.extras is different than null — or in other words, the intent contains some data.

The data that you pass with Intent when starting a new activity is preserved when the activity is being recreated.

To wrap up this section, tap back while the screen is in landscape orientation and observe the logs once again.

Logs showing activity lifecycle when going back from ShareActivity to MainActivity in landscape orientation

ShareActivity is paused and old portrait MainActivity is destroyed. Then, new landscape MainActivity is created and resumed. Finally, the app calls ShareActivity‘s onStop() and onDestroy().

Great job getting to this point! Now that you understand the activity lifecycle and how to correctly manage the activity state, it’s time to move on to fragments. :]

Note: If you want to learn more about activities, check out the Introduction to Android Activities with Kotlin tutorial. It covers similar topics, but from a slightly different angle. It also introduces a couple new concepts.