Unlocking the Secrets of Android Activity Lifecycle: What Really Happens Behind the Scenes.
We have reached the eighth stage of our journey into the deep, hidden and intricate world of Android. In the previous articles we have seen that ActivityManagerService (AMS) plays a central role in managing application components like activities and tasks.
Activities and Tasks are closely related concepts. An Activity represents a single screen with a user interface in an app while a Task is a logical structure containing an ordered set of Activities, representing a workflow the user is performing. Activities in a Task are organized into a stack called the Back Stack, managed using a Last-In, First-Out (LIFO) principle. When a new Activity is started, it is "pushed" onto the stack. When the user presses the "Back" button, the most recent Activity is "popped" off the stack and destroyed, revealing the previous one.
The AMS orchestrates the activity lifecycle and task switching.
In this article we will explore the role of the Activity class, its dependencies on the Context class, its lifecycle and its relationship with the Task structures managed by the Activity Manager service (AMS).
Let's start by defining what an Activity is. According to the official documentation, an Activity is described as "one screen in the application".
However, this definition is often lacking, as some apps can consist of a single activity that handles multiple screens or complex functionality.
Another definition suggests that an activity is as "entry points for user interaction", but this is too abstract and not entirely accurate.
We can say that “What is Activity” isn’t a simple question!
Let’s try to find a better definition for Activity.
We consider having a device with a screen, this is a typical Android device. On this device, there are multiple Android applications. Let’s say one of them is our application, which has its own process and runs on this device. Now, suppose our application wants to render something on the screen. How will it achieve that?
?
?
Well, Android Framework provides a specific class for this. And this class is called Activity
In essence, an Activity is a bridge between an Android application and the device's screen. It acts as a window controller, managing how an app displays content on the screen.
Since the screen is a shared resource, multiple applications (and their activities) often compete for control. The Android operating system manages this competition and ensures seamless transitions by notifying activities when they gain or lose control of the screen.
The Activity Lifecycle handles these transitions and interactions, defining when an activity is created, destroyed, or undergoes state changes.
In practice the activity lifecycle is a complex sequence of methods that govern these processes. For instance, methods like onCreate(), onStart(), onResume(), and others are invoked at different stages.
Here is a simplified version of the activity lifecycle.
?
Before diving into the activity lifecycle topic, it’s important to address a common source of confusion for Android developers: the relationship between Activity and Context.
Activities extend the Context class, which serves as an interface to the application environment, providing access to resources, application-level operations, and integration with the Android system. This makes Context a central point of interaction between the app and the Android framework.
Context class and its derivatives, including Activity, are very complex. The Activity class itself extends several layers of classes (ContextThemeWrapper, ContextWrapper, and Context), resulting in over 10,000 lines of code. The complexity comes from the fact that Context has many responsibilities, such as managing permissions, accessing files, and interacting with the system. As a result, Context is often considered a "God object" a class that knows and does too much. Unfortunately, because Activity inherits from Context, it also takes on these responsibilities, adding unnecessary complexity to its primary role.
In his book "Clean Code: A Handbook of Agile Software Craftsmanship" Robert Martin talks about the "God class" and calls it out as a prime example of a violation of the "classes should be small" rule he uses to promote the Single Responsibility Principle". (1.), (2.)
?
This inheritance design of Activity contradicts the principle of "favor composition over inheritance" (3.), (4.) leading to accidental complexity rather than essential complexity. While activities function as window or UI controllers, their extension of Context blurs this role, adding layers of responsibility unrelated to their lifecycle or core purpose.
Understanding that Context and Activity are orthogonal concepts is crucial. The activity’s main role is to serve as a window controller, and its relationship with Context is more of a legacy decision than a fundamental necessity.
With this clarified, we can now focus on the core topic of this article: the activity lifecycle and its primary responsibilities in Android applications.
So let’s delve into the lifecycle of an Android activity, focusing on two key methods: onCreate() and onDestroy(). These methods form a complementary pair, as we’ll see.
onCreate / onDestroy methods:
When you request Android to open an activity, the system creates and instantiates it for you. You cannot call an activity's constructor directly. Instead, the system invokes the onCreate() method immediately after instantiation. This method serves as a substitute for the constructor, where you should place all initialization logic. In onCreate:
The onDestroy method is called when the system decides the activity is no longer needed. However, it doesn't guarantee immediate destruction of the activity. Think of it as the system releasing the activity from active use. A better name might be onRelease ??.
Typically, we override onDestroy only to unregister listeners or observers registered in onCreate or perform lightweight cleanup tasks.
In most cases, overriding onDestroy isn’t necessary unless specific logic demands it.
Let's see onCreate and onDestroy in action through an example:
Consider a simple app with two activities:
In MainActivity.onCreate, the process involves:
Neither activity initially overrides onDestroy, as this is rarely required. However, for logging purposes, we might override onDestroy to track when these methods are invoked.
here is the code:
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
Log.i("LifeCycleTest","MainActivity onCreate()")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btnNextActivity).setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
}
override fun onDestroy() {
Log.i("LifeCycleTest","MainActivity onDestroy()")
super.onDestroy()
}
}
class SecondActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
Log.i("LifeCycleTest","SecondActivity onCreate()")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
}
override fun onDestroy() {
Log.i("LifeCycleTest","SecondActivity onDestroy()")
super.onDestroy()
}
}
Let's make some observations about life cycle behavior: When navigating from MainActivity to SecondActivity, the method onCreate is called for SecondActivity. However, onDestroy for MainActivity is not invoked. Returning to MainActivity with a Back Gesture (swipe on the side screen for Android 12 and later or back button in navigation bar for previous Android versions) triggers onDestroy for SecondActivity but does not reinitialize MainActivity through its onCreate.
Here is the sequence of screens for our example:
Here is the device logcat image. It's proof of what I told you.
This demonstrates how Android manages the back stack, which we’ll explore very shortly, but first I want to give you some guidelines on what to do and what not to do in the two methods seen. Here is the list of Best Practices:
Do in onCreate(): Always call super.onCreate - Use setContentView to bind the UI - Initialize essential fields and properties and modify attributes affecting the UI layout.
Avoid in onCreate(): Starting animations or long running processes (better suited for onStart or onResume) - Fetching data or initiating complex operations - Allocating heavy resources.
Do in onDestroy(): Unregister any listeners or observers registered in onCreate.
Avoid in onDestroy(): Overriding it unnecessarily.
By adhering to these guidelines, you can ensure efficient and error-free activity lifecycle management in your applications.
?
Let's return to the topic of Android's activity lifecycle management and dive into the exploration of a fundamental element for understanding the behavior of activities: the activities Backstack. We've already seen this concept in action when we saw that the onDestroy() callback is not guaranteed to be invoked, but it's time to explain it in more detail:
Imagine a timeline moving left to right. When you launch Activity 1, it’s added to an internal stack structure in memory. Launching Activity 2 pushes it on top of the stack, making it the foreground activity, while Activity 1 becomes inactive but remains in the Backstack. The stack operates on a Last In, First Out (LIFO) principle.
For example:
This image should make it clear what happens to the activities into stack following the operations described above.
When the back button or a navigation gesture is used:
Also this image should make it clear what happens to the activities into stack following the operations described above.
Finally, pressing back from Activity 1 either:
?
Foreground activities are visible and interactive, while backstack activities are inactive and not visible. Importantly:
As developers, it’s critical to handle these states properly:
To manage these transitions, Android provides additional lifecycle methods beyond onCreate and onDestroy. These other methods will allow us to differentiate foreground and backstack activities effectively.
Let's start with the onStart() and onStop() lifecycle methods. These methods provide essential information that onCreate() and onDestroy() cannot specifically, the distinction between a backstack activity and a foreground activity.
onStart / onStop methods:
?
Let’s take an example:
First of all in our two activities, MainActivity and SecondActivity I added some logs to the onStart() and onStop() methods, this will help us understand their behavior well,
here is the code:
领英推荐
// class MainActivity
override fun onStart() {
Log.i("LifeCycleTest","MainActivity onStart()")
super.onStart()
}
override fun onStop() {
Log.i("LifeCycleTest","MainActivity onStop()")
super.onStop()
}
// class SecondActivity
override fun onStart() {
Log.i("LifeCycleTest","SecondActivity onStart()")
super.onStart()
}
override fun onStop() {
Log.i("LifeCycleTest","SecondActivity onStop()")
super.onStop()
}
Now let's repeat this flow on our app:
Here is the device logcat image for our example. It's proof of what I told you.
Now, let's revisit an interesting nuance about the activity lifecycle that many Android developers overlook: The onStart() method is not triggered when an activity comes to the foreground, but rather when it gains control over any visible portion of the screen. Similarly, onStop() is not called when an activity moves to the back stack, but only when it loses control of all visible portions of the screen.
How can we observe and demonstrate this behavior? A simple way is to create a third activity: a full-screen transparent activity (TransparentActivity) that overlays the SecondActivity. This allows the SecondActivity to remain visible while the transparent activity sits on top of it. This setup will show that even though the transparent activity is on top of the stack, the second activity does not go through onStop because it remains visible.
Here’s how it works:
this is the added code:
// class SecondActivity
override fun onCreate(savedInstanceState: Bundle?) {
Log.i("LifeCycleTest","SecondActivity onCreate()")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
findViewById<Button>(R.id.btnTransparentActivity).setOnClickListener {
val intent = Intent(this, TransparentActivity::class.java)
startActivity(intent)
}
}
class TransparentActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
Log.i("LifeCycleTest","onCreate()")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_transparent)
}
override fun onDestroy() {
Log.i("LifeCycleTest","TransparentActivity onDestroy()")
super.onDestroy()
}
override fun onStart() {
Log.i("LifeCycleTest","TransparentActivity onStart()")
super.onStart()
}
override fun onStop() {
Log.i("LifeCycleTest","TransparentActivity onStop()")
super.onStop()
}
}
When the app runs, the following happens:
Now, the interesting part:
Here is the device logcat image. It's proof of what I told you.
Why doesn’t the second activity stop? This is the key nuance: onStart and onStop are tied to visibility rather than whether an activity is in the foreground or background. Android recognizes that the second activity is still partially visible under the transparent activity, so it stays in the started state.
At this moment, both the second activity and the transparent activity are in the started state simultaneously.
Here is the sequence of screens for our example:
When you navigate back:
This demonstration highlights a critical detail about activity lifecycle methods. Many developers assume onStart() and onStop() method are tied to foreground and background transitions, but they’re actually tied to visibility. This is an important distinction that gives you deeper control over your app’s lifecycle.
Now I want to give you some guidelines on what to do and what not to do into onStart() and onStop() methods seen. Here is the list of Best Practices:
Do in onStart(): Call super.onStart() to prevent crashes. - Register observers for UI-related updates. - Refresh and update the UI with the latest data. - Initiate functional flows like fetching data.
Avoid in onStart(): Start animations unnecessarily (we'll see it soon). Access mutually exclusive resources like the camera.
Do in onStop(): Call super.onStop() to avoid crashes. - Unregister observers initialized in onStart. - Pause or cancel long-running UI-related computations to save resources.
Avoid in onStop(): Allow unnecessary processes to continue in the background.
The onStart() and onStop() methods are crucial for managing activity visibility and ensuring resource efficiency. They complement onCreate() and onDestroy() by handling transitions between active and inactive states.
After exploring onCreate/onDestroy and onStart/onStop, it might seem like there’s nothing left to discuss about the activity lifecycle. However, onResume and onPause play crucial roles.
onResume / onPause methods:
First of all, I will just tell you when these methods are being invoked.
These simple definitions have significant implications.
So let's jump into the test application and I will show you when these two methods are absolutely required.
In my demo app, I added a progress animation to the SecondActivity. I start it in the onCreate() method (because it is defined as visible in the layout file) and stop it in the onStop() method (where its visibility is set to GONE).
Here is the new code of SecondActivity class.
// SecondActivity
private lateinit var progress: View
override fun onCreate(savedInstanceState: Bundle?) {
Log.i("LifeCycleTest","SecondActivity onCreate()")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
progress = findViewById<View>(R.id.progress)
findViewById<Button>(R.id.btnTransparentActivity).setOnClickListener {
val intent = Intent(this, TransparentActivity::class.java)
startActivity(intent)
}
}
override fun onStop() {
Log.i("LifeCycleTest","SecondActivity onStop()")
super.onStop()
progress.visibility = View.GONE
}
When navigating from MainActivity to SecondActivity and to TransparentActivity that overlays the SecondActivity, the progress animation continues because onStop is not called (the SecondActivity remains visible).
Here is the sequence of screens where you can clearly see the issue.
To fix this, we use onPause() method to stop the animation when the activity becomes non-interactive and onResume() to restart it when the activity regains interactivity. For example:
This ensures that the animation is paused during overlays and resumed when the activity is visible again.
Here is how the code for SecondActivity should be done:
// SecondActivity
private lateinit var progress: View
override fun onCreate(savedInstanceState: Bundle?) {
Log.i("LifeCycleTest","SecondActivity onCreate()")
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
progress = findViewById<View>(R.id.progress)
findViewById<Button>(R.id.btnTransparentActivity).setOnClickListener {
val intent = Intent(this, TransparentActivity::class.java)
startActivity(intent)
}
}
override fun onResume() {
Log.i("LifeCycleTest","SecondActivity onResume()")
super.onResume()
progress.visibility = View.VISIBLE
}
override fun onPause() {
Log.i("LifeCycleTest","SecondActivity onPause()")
super.onPause()
progress.visibility = View.GONE
}
and this is the sequence of navigation screens that you have with this latest version of code. It is proof that everything is working fine now.
And here is the logcat image.
So to summarize we have seen that the key differences of onStart vs. onResume are:
When transitioning between activities:
This sequence ensures only the top activity remains interactive (resumed).
Now I want to give you some guidelines on what to do and what not to do into onResume() and onPause() methods seen. Here is the list of Best Practices:
Do’s:? Always call super.onResume and super.onPause to avoid crashes. - Use onResume to start animations, access shared resources (e.g., the camera), or update UI elements. - Use onPause to stop animations, release shared resources, or pause ongoing tasks.
Don’ts: Avoid overriding onResume or onPause unnecessarily, use onStart/onStop for most lifecycle-related tasks.- Misusing these methods can introduce subtle bugs, especially when activities share resources or animations.
By understanding the nuances of onResume and onPause, you’ll be better equipped to manage activity interactivity and transitions effectively. Remember, these methods are essential for tasks directly tied to the user experience when an activity is in the foreground.
That's it for this topic.
At this point, you likely know more about activity lifecycles than 99% of professional Android developers! So you are ready for the next challenging episode of this journey into the deep, hidden and intricate world of the Android operating system.
In the next episode we will dive deeper into the role of AMS in managing the lifecycle of Activities and data structures like ActivityRecord and Tasks.
I remind you my newsletter "Sw Design & Clean Architecture"? : https://lnkd.in/eUzYBuEX where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.
Thanks for reading my article, and I hope you have found the topic useful,
Feel free to leave any feedback.
Your feedback is very appreciated.
Thanks again.
Stefano
References:
1. Robert Martin, “Clean Code: A Handbook of Agile Software Craftsmanship” - Pearson, August 2008 (page 136).
2. S.Santilli, "Single Responsibility Principle".
3. S.Santilli, "Inheritance vs Composition: which is the best solution?".
That sounds like a deep dive into the Android world. Which part of the Activity lifecycle is tripping you up?