Jetpack Compose, First Edition — Updated for Latest Beta!

The absolute best book to bring the power of Jetpack Compose to your Android projects.
Up-to-date for Kotlin 1.4, Android Studio 4.1, and the latest Android Canary.
Start reading for free — today!

Home Android & Kotlin Tutorials

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.

5/5 5 Ratings

Version

  • Kotlin 1.4, Android 10.0, Android Studio 4.2

When it comes to building an Android app, activities and fragments are key components to building its user interface (UI). As the user navigates within the app, these components go through different states of the Android lifecycle.

Understanding the lifecycle and responding correctly to its state changes is crucial. It allows you to build apps that have fewer bugs, use fewer Android system resources and provide a good user experience.

In this tutorial, you’ll play with a simple app that you can use to count dogs as you walk around the neighborhood: PuppyCounter. You’ll learn:

  • The role of the Android lifecycle in apps.
  • The basics of the activity and fragment lifecycles, and the callbacks invoked when the activity moves between lifecycle states.
  • How to save and restore the activity instance state.
  • The process for passing the data between activities and fragments.
  • How ViewModel can help you store and manage UI-related data in a lifecycle-conscious way.

For this tutorial, you need Android Studio and an Android device or emulator.

Getting Started

To start, download the materials by using the Download Materials button at the top or bottom of this tutorial. Open the starter project in Android Studio. Once the project opens, let it build and sync, and you’ll be ready to go!

Run the app and check its features:

The main and share screens in the PuppyCounter app

The app has two screens:

  • Main screen: Allows you to count dogs of different sizes. You can tap the cards or the plus and minus buttons to update the counters. In the top bar, you have two buttons: The first one resets the values and the second one opens the Share screen.
  • Share screen: Displays your dog count. Tap Share to open a dialog that asks you if you want to share your count. In this tutorial, you won’t implement that logic, but you can pretend that it exists. :]

Next, check out the project structure:

Puppy Counter app's project structure

As you can see, a lot is already prepared for you. The most important packages are activities and fragments. You’ll move between them as you learn about activity and fragment lifecycle. Right now, don’t worry about the details. You’ll become familiar with classes inside as you go through the tutorial. Besides those packages, notice these three things:

  • DogCount: a model class for your data.
  • SplashActivity: a splash activity that opens up when you start the app.
  • PuppyCounterApplication: the app class.

Before going into details of the activity lifecycle, here’s some background on the lifecycle’s role in Android apps.

Understanding the Role of the Lifecycle in Apps

The Android operating system (OS) is a multi-user Linux system. In most cases, every app runs in its own Linux process. The OS creates the process when any of the app components need to execute. When no app component is running and the OS needs to free memory to run other apps, it kills the process.

The Android OS uses an importance hierarchy to determine which processes to keep alive or to kill. The importance hierarchy categorizes processes as different types. That type depends on the app components that are currently running and their current state.

The most common app component is Activity. Every Android app has one or more activities. As the user navigates within the app, activities go through different lifecycle states.

Developers must understand how different components impact the process’s lifetime. Not using these components correctly can result in the system killing the process while it’s doing important work.

Exploring the Activity Lifecycle

Activity lifecycle and lifecycle callbacks

The figure above shows different states that activity goes through during its lifecycle:

  • Initialized: Activity instance is created and its properties initialized.
  • Created: Activity is now completely initialized and ready to configure its UI.
  • Started: Activity is visible to the user.
  • Resumed: Activity is visible to the user and has focus. In this state, the user is likely interacting with the activity.
  • Destroyed: Activity is destroyed and the OS can reclaim its memory.

Notice different callbacks between states. The OS invokes these callbacks when the activity moves from one state to another. You can override those methods in your own activities to perform tasks in response to those lifecycle state changes.

Before explaining each callback, check them out in action. Open MainActivity.kt and add logging to onCreate() and onResume(). Override other callbacks as well:

override fun onStart() {
  Timber.i("PuppyCounter - MainActivity - onStart()")
  super.onStart()
}

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

  findViews()

  setupSmallDogViewsClickListeners()
  setupMiddleDogViewsClickListeners()
  setupBigDogViewsClickListeners()
}

override fun onResume() {
  Timber.i("PuppyCounter - MainActivity - onResume()")
  super.onResume()
  renderDogCount(dogCount)
}

override fun onPause() {
  Timber.i("PuppyCounter - MainActivity - onPause()")
  super.onPause()
}

override fun onStop() {
  Timber.i("PuppyCounter - MainActivity - onStop()")
  super.onStop()
}

override fun onDestroy() {
  Timber.i("PuppyCounter - MainActivity - onDestroy()")
  super.onDestroy()
}

Whenever you override a callback like this, make sure that you call the super class method as well. If you don’t do that, your app won’t execute some important work and it might crash or end up in a weird state.

Build and run the app. Then, check the logs. To inspect the logs in Android Studio, open the Logcat tool by clicking Logcat at the bottom. Enter PuppyCounter in Search History to show the results:

Arrow pointing to Logcat tool in Android Studio

Next, close the app by tapping back, or swipe back if you have gestural navigation enabled. Check the logs again. You should see something like this:

Logs showing full activity lifecycle

Note: You won’t see “Back button clicked” in your logs. That’s added to make it easier for you to spot the button taps.

Understanding Activity Lifecycle Callbacks

You just went through one complete activity lifecycle: Activity was created, resumed and finally destroyed when you exited the app.

Diagram of activity lifecycle callbacks

The diagram above represents an activity lifecycle:

  • onCreate(): Activity enters the Created state. Here, you perform logic that should happen only once for the entire life of the activity. This can include setting the content view, associating the activity with a ViewModel, instantiating some class-scope variables, etc.
  • onStart(): Activity enters the Started state. This call makes the activity visible to the user, as the app prepares for the activity to enter the foreground and become interactive.
  • onResume(): Activity enters the Resumed state. The user can now interact with the activity. Here, you can enable any functionality that needs to run while the component is visible and in the foreground.
  • onPause(): Activity enters the Paused state. This call indicates that the activity is no longer in the foreground, though it may still be visible if the user is in multi-window mode, for example. During this time, you should pause or adjust operations that shouldn’t continue or should continue in moderation. The activity remains in this state until either the activity resumes — for example, opening or closing the bottom sheet in the activity — or until it becomes completely invisible to the user — such as when opening another activity.
  • onStop(): Activity enters the Stopped state. Activity is no longer visible to the user. Here, you should release or adjust resources that aren’t needed while the activity isn’t visible to the user. You should also use this opportunity to perform shutdown operations on tasks that are relatively CPU-intensive, like operations on the database, for example.
  • onDestroy(): Activity enters the Destroyed state. Here, the activity is finishing. This can be because:
    • The user is completely dismissing the activity.
    • finish() is being called on the activity.
    • The system is temporarily destroying the activity due to a configuration change, such as device rotation or multi-window mode.

    There are situations where the system will kill a process hence not calling onDestroy() or any other activity lifecycle methods. So, it should not be used to do things that are intended to remain around after the process goes away.

Note: For more information about the Activity lifecycle, refer to the Android Developer documentation.

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.

Exploring Fragment Lifecycle

Like activities, fragments have their lifecycle. When the user navigates and interacts with your app, your fragments transition through various states in their lifecycle as they’re added, removed, and enter or exit the screen.

Fragment lifecycle and fragment lifecycle callbacks

In the figure above, you can see that fragment lifecycle is similar to the activity lifecycle but contains some additional fragment-specific methods. Before explaining each callback, check them in the app.

In the previous section, you played with two activities and saw how their lifecycle changed when you moved between screens. For this example, you’ll implement the same screens with fragments. You can find two fragments that represent each screen in the fragments package: MainFragment.kt and ShareFragment.kt. There’s also one container activity and viewmodels package. For now, ignore the viewmodels package. You’ll need that in the next section.

If you check MainFragment.kt, you’ll notice a lot of similarities with MainActivity.kt. They both share the same logic for managing states, but MainFragment.kt contains a few more lifecycle callbacks.

Before running the app, open SplashActivity.kt and update startFirstActivity() so that it starts ActivityWithFragments instead of MainActivity:

private fun startFirstActivity() {
  startActivity(Intent(this, ActivityWithFragments::class.java))
}

Excellent! Now, build and run the app. Then, inspect the logs.

Logs showing activity and fragment lifecycle when opening Main screen implemented with fragment

Notice how the fragment lifecycle is synchronized with the activity lifecycle. First, the app creates and starts ActivityWithFragments. After that, it creates and starts the fragment and its view. Finally, it resumes both activity and fragment.

Next, tap back and again observe the logs.

Logs showing activity and fragment lifecycle when pressing back button on Main screen

By closing the app, you started the process of destroying the activity. As before, fragment lifecycle events follow activity lifecycle events. Both activity and fragment are first paused, then stopped and finally destroyed.

A fragment’s lifecycle state can never be greater than its parent. For example, a parent fragment or activity must start before its child fragments. Likewise, child fragments must stop before their parent fragment or activity. From the logs above, you might think otherwise — that the activity is stopping first — but that’s just because you print the log as the first call in lifecycle callbacks. Internally, the OS makes sure that all the child fragments stop before stopping the activity.

Understanding Fragment Lifecycle Callbacks

You can now dive a bit deeper into every lifecycle event to get a better understanding of the fragment lifecycle:

  • onCreate(): The fragment reaches the Created state. Similar to the activity’s onCreate(), this callback receives Bundle, containing any state previously saved by onSaveInstanceState().
  • onCreateView(): It’s called to inflate or create the fragment’s view.
  • onViewCreated(): The fragment’s view is instantiated with a non-null View object. That view is set on the fragment and can be retrieved using getView().
  • onStart(): The fragment enters the Started state. In this state, it’s guaranteed that the fragment’s view is available and that it’s safe to perform FragmentTransaction on the child FragmentManager of the fragment.
  • onResumed(): The fragment enters the Resumed state. It becomes visible after all Animator and Transition effects have finished. The user is now able to interact with the fragment.
  • onPause(): The fragment goes back to the Started state. The OS invokes this callback as the user begins to leave the fragment while the fragment is still visible.
  • onStop(): The fragment goes back to the Created state and is no longer visible.
  • onDestroyView(): It’s triggered after all the exit animations and transitions have completed, when the fragment’s view has been detached from the window. At this point, all references to the fragment’s view should be removed, allowing the fragment’s view to be garbage collected.
  • onDestroy(): The fragment enters the Destroyed state. It happens when the fragment is removed or if FragmentManager is destroyed. At this point, the fragment has reached the end of its lifecycle.

Now that you understand better what goes under the hood, navigate between the Main and Share screens to see the dance of the fragment lifecycle. :]

Logs showing activity and fragment lifecycle when going from Main screen to Share screen

As you’ve seen in this section and the previous one, the Android lifecycle is quite complex. Managing states and interacting with the UI at the right time can be tricky for inexperienced developers. This led to some new Android APIs and components that should make life easier for all Android developers. One such component is the ViewModel.

Note:

Using ViewModel to Store UI Data

ViewModel is designed to store and manage UI-related data in a lifecycle-conscious way.

First, replace the logic for saving and restoring state in MainFragment.kt with the approach that uses ViewModel.

In the viewmodels package, create a new class called MainViewModel.kt.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.raywenderlich.android.puppycounter.model.DogCount

class MainViewModel : ViewModel() {

  private val _dogCount: MutableLiveData<DogCount> by lazy {
    MutableLiveData<DogCount>(DogCount())
  }
  val dogCount: LiveData<DogCount> = _dogCount

  fun setDogCount(dogCount: DogCount) {
    _dogCount.value = dogCount
  }
}

This will be ViewModel for your Main screen. You’ll use it to save DogCount‘s state. Use _dogCount for tracking the state and dogCount for exposing it to the observers. For updating the state, use setDogCount().

Note: If you want to learn more about LiveData, refer to the Android Developer documentation.

In the MainFragment.kt, add these imports:

import androidx.fragment.app.viewModels
import com.raywenderlich.android.puppycounter.fragments.viewmodels.MainViewModel

Add the following line above onCreate():

private val viewModel: MainViewModel by viewModels()

With this, you added the code to create MainViewModel for MainFragment.

Next, add the following method to MainFragment.kt:

private fun subscribeToViewModel() {
  viewModel.dogCount.observe(viewLifecycleOwner, { value ->
    dogCount = value
    renderDogCount(dogCount)
  })
}

This method allows you to subscribe to the observable state in MainViewModel. Whenever dogCount‘s state changes, the app pushes the new state to the view and renderDogCount() is called with the new state, resulting in the updated UI.

Then, modify onViewCreated() so you subscribe to MainViewModel after the super call:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  Timber.i("PuppyCounter - MainFragment - onViewCreated()")
  super.onViewCreated(view, savedInstanceState)
  subscribeToViewModel()
  findViews(view)
  setupSmallDogViewsClickListeners(view)
  setupMiddleDogViewsClickListeners(view)
  setupBigDogViewsClickListeners(view)
}

You also need the logic that will update the state in MainViewModel. Modify updateDogCount() like this:

private fun updateDogCount(newDogCount: DogCount) {
  viewModel.setDogCount(newDogCount)
}

This method is called whenever the user updates the counters. It’ll update MainViewModel with the new state. MainViewModel will push that state through dogCount and the MainFragment will be notified since it’s subscribed to the MainViewModel.

Finally, in MainFragment remove onSaveInstanceState(), renderDogCount(dogCount) call from the onResume() and the savedInstanceState?.run { ... } code in onCreate(). You no longer need that. :]

Build and run the app. Tap the counters a couple times and rotate the screen. You should see something like this:

Main screen in both orientations showing how state was preserved by using ViewModel

Check the illustration below to see ViewModel‘s lifetime next to the associated activity lifecycle.

ViewModel lifetime next to the associated activity lifecycle

ViewModel objects are scoped to the Lifecycle passed to the ViewModelProvider when getting ViewModel. It remains in memory until the Lifecycle that it’s scoped to goes away permanently. For an activity, that happens when it’s finished. For a fragment, that happens when it’s detached.

Where to Go From Here?

Download the final project by using the Download Materials button at the top or bottom of this tutorial.

Now that you know the basics of the Android lifecycle, you’re ready to explore further. Build your apps and don’t be afraid to inspect how your activities and fragments behave in different scenarios. Besides ViewModel, other components can also help you to manage the lifecycle, such as Lifecycle-Aware Components. You can also look into more complex cases, like the Multi-Window Lifecycle.

Cover image of Android Apprentice book

You’re now ready to dive deep into whatever state of the Android lifecycle you want. If you liked what you learned in this tutorial, check out the complete Android Apprentice book. It takes you all the way from building your first app to submitting your app for sale. A couple chapters cover what you learned here, but the difference is that you’ll build the apps from the ground up.

Hopefully, you enjoyed this tutorial. If you have any questions or comments, please join the discussion below! :]

Average Rating

5/5

Add a rating for this content

5 ratings

More like this

Contributors

Comments