Unlocking the Secrets of Android Activity Lifecycle: What Really Happens Behind the Scenes.

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?

?

Fig1: Screen is a shared resources.

?

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.

Fig2: A simplified illustration 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.)

?

Fig3: Activity inherits from Context hierarchy.


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:

  1. Call super.onCreate(): Mandatory to avoid runtime exceptions.
  2. Set the layout: Use setContentView() to bind a user interface (often defined in XML) to the activity’s window.
  3. Initialize properties: Set up fields or components required for the activity, just like you would in a constructor.


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:

  • MainActivity: Contains a button to navigate to the second activity.
  • SecondActivity: Contains no UI elements.

In MainActivity.onCreate, the process involves:

  1. Calling super.onCreate.
  2. Setting the layout with setContentView.
  3. Initializing properties, such as a button and setting its click listener.

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:

  1. Launch Activity 1 → Added to the stack.
  2. Launch Activity 2 → Pushed on top; Activity 1 is now inactive.
  3. Launch Activity 3 → Pushed on top; Activity 1 and Activity 2 remain inactive.

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:

  1. Activity 3 is popped from the stack and destroyed.
  2. Activity 2 becomes the active foreground activity.
  3. Returning to Activity 1 pops Activity 2, destroying it.

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:

  • Moves the entire stack to the background, showing the home screen.
  • Pops and destroys Activity 1, leaving the stack empty (for Android 12+: the last activity remains in memory as inactive for faster reactivation).

?

Foreground activities are visible and interactive, while backstack activities are inactive and not visible. Importantly:

  • All backstack activities remain in the created state (onCreate called, but not onDestroy).
  • Foreground activities differ by being actively displayed.

As developers, it’s critical to handle these states properly:

  • Stop UI rendering for backstack activities to save resources and prevent crashes.
  • Ensure UI modifications occur only in foreground activities.

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:

  • onStart: Called when an activity gains control over any visible portion of the screen, not necessarily when it becomes the foreground activity. This nuance is often misunderstood but crucial.
  • onStop: Called when an activity loses control over all visible parts of the screen, meaning it is no longer visible to the user.

?

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:

  1. Launching Main Activity: onCreate and onStart are called because the activity is visible.
  2. Switching to Second Activity: onCreate and onStart are called for the second activity (now visible). onStop is called for the main activity (now invisible).
  3. Navigating Back to Main Activity: onStart is called again for the main activity without recreating it, onStop is called for the second activity, and it is later destroyed.

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:

  • In addition to the main and second activities, we now have a transparent activity.
  • The second activity includes a button to navigate to the transparent activity.
  • The transparent activity is styled with a special theme in the manifest to make its background semi-transparent (e.g., no title, fully transparent background).

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:

  1. Main Activity: Upon launch, onCreate and onStart are called.
  2. Second Activity: Navigating to it triggers its onCreate and onStart, while the main activity is stopped.

Now, the interesting part:

  • Navigating to the TransparentActivity shows it over the SecondActivity.
  • While the SecondActivity is no longer interactive (covered by the transparent activity), it remains visible. In the logs, you’ll notice: The TransparentActivity’s onCreate and onStart are called. The SecondActivity does not go through onStop.


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:

  • The transparent activity calls onStop and onDestroy.
  • The second activity does not call onStart again because it was never stopped.
  • Navigating back to the main activity triggers onStart for the main activity and onStop for the second activity.

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.

  • onResume: Called when the activity becomes interactive (typically when it’s at the top of the activity stack).
  • onPause: Called when the activity becomes non-interactive (no longer at the top of the stack).

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:

  • onPause: Hide the animation.
  • onResume: Restart the animation.

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:

  • onStart: Multiple activities can be in a started state simultaneously.
  • onResume: Only one activity can be resumed at a time.

When transitioning between activities:

  1. The current activity enters onPause.
  2. The new activity completes onCreate and onStart, followed by onResume.
  3. The previous activity then transitions to onStop.

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?".

4. S.Santilli, "Composite design pattern: the Ancient Roman Empire's secret power".

That sounds like a deep dive into the Android world. Which part of the Activity lifecycle is tripping you up?

回复

要查看或添加评论,请登录

Stefano Santilli的更多文章

社区洞察

其他会员也浏览了