Avoiding Memory Leaks and Lag: The Android Lifecycle Demystified

Glory OlaifaGlory Olaifa
10 min read

As an Android developer, one of the most important things you can learn is how to manage the Activity Lifecycle.

But what does that mean?

Think of your app like a house party. People arrive (your app opens), they mingle (interact), they leave (background the app), or the house shuts down (they close it). If you don’t clean up after each stage, things pile up — memory leaks, sensor drain, lag, crashes.

This article will help you understand how to manage those resources properly using Jetpack Compose, Android’s modern UI toolkit. We’ll use real code examples to show:

  • When and where to load resources like images, sensors, and media players.

  • How to clean up after yourself so your app doesn’t slow down or crash.

  • How to observe lifecycle events with Compose and LifecycleObserver.

1. 🚧 Understanding the Android Lifecycle (with Compose in Mind)

Before we write a single line of code, let’s understand the life story of an Android screen (Activity).

An Activity in Android is like a living thing — it’s born, grows, becomes active, goes to sleep, and eventually dies. This is called the lifecycle, and managing it properly is key to avoiding memory leaks and performance issues.

onCreate()
   ↓
onStart()
   ↓
onResume()
   ↑ ↓    ← user is interacting here
onPause()
   ↓
onStop()
   ↓
onDestroy()

What Each Lifecycle Method Does

MethodWhen It HappensWhat You Should Do Here
onCreate()When the activity is first createdSetup things: UI, state, sensors, media player
onStart()Just before the activity becomes visibleStart lightweight tasks like tracking, analytics
onResume()Right when the activity is interactiveStart animations, sensors, playback — the user is looking at it
onPause()Activity loses focus (e.g., a dialog appears)Pause animations, stop sensors, save transient data
onStop()Activity is no longer visibleRelease big stuff like bitmaps or large objects
onDestroy()Activity is about to be destroyed permanentlyClean up everything — unregister listeners, close resources
  • Explain onCreate, onStart, onResume, onPause, onStop, onDestroy

  • How Jetpack Compose fits in (i.e., no onCreateView, but lifecycle is still relevant)

2. Step-by-Step Code Breakdown

MainActivity.kt: Managing Heavy Resources

In this section, we prepare and load the resources: a bitmap image, a media player, and a sensor manager, all inside MainActivity. Then we use Jetpack Compose’s setContent {} block to load our composable screen.

Step 1: MainActivity Setup

class MainActivity : ComponentActivity(), SensorEventListener {
  • ComponentActivity is the base class for Compose activities.

  • We implement SensorEventListener to listen to accelerometer data (optional, but educational).

Step 2: Define Your Variables

private lateinit var sensorManager: SensorManager
private var accelerometer: Sensor? = null
private var mediaPlayer: MediaPlayer? = null
private var largeBitmap: Bitmap? = null
  • SensorManager helps us access physical sensors like the accelerometer.

  • MediaPlayer is used to play a .mp3 sound.

  • Bitmap simulates a heavy memory object (like a large image file).

Step 3: onCreate() — Initializing Resources

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
    accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

    mediaPlayer = MediaPlayer.create(this, R.raw.sound_effect)

    largeBitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image)

Here’s what happens:

LinePurpose
sensorManagerGets the Android system’s sensor service
accelerometerGets the default accelerometer sensor
MediaPlayer.create(...)Loads a .mp3 from res/raw
BitmapFactory.decodeResource(...)Loads a big image file from res/drawable into memory

💡 We do all of this in onCreate() because it’s the first and safest place to initialize things your activity needs.

Step 4: setContent {} — Launching the UI

    setContent {
        ComposeLifecycleTheme {
            Surface(color = MaterialTheme.colors.background) {
                LifecycleAwareScreen(
                        onResume = {
                            Log.d("LifecycleDemo", "onResume → Sensor ON + Media Play")
                            sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL)
                            mediaPlayer?.start()
                        },
                        onPause = {
                            Log.d("LifecycleDemo", "onPause → Sensor OFF + Media Pause")
                            sensorManager.unregisterListener(this)
                            mediaPlayer?.pause()
                        },
                        onStop = {
                            Log.d("LifecycleDemo", "onStop → Bitmap recycle")
                            largeBitmap?.recycle()
                            largeBitmap = null
                        },
                        onDestroy = {
                            Log.d("LifecycleDemo", "onDestroy → Media release")
                            mediaPlayer?.release()
                            mediaPlayer = null
                        }
                    )
            }
        }
    }
}
  • This loads the Jetpack Compose UI.

  • We pass lifecycle-aware callbacks into LifecycleAwareScreen that we’ll hook into the Android lifecycle using DisposableEffect.

Step 5: SensorEventListener Stubs

We also need to stub out these required methods since we implement SensorEventListener:

override fun onSensorChanged(event: SensorEvent?) {
    // You can log or use the accelerometer data here
}

override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    // Not needed for this example
}

We do the heavy lifting only once, and use Compose to react to lifecycle changes later. This separation is powerful and modern.

Now let’s talk about where we’ll observe Lifecycle Events

LifecycleAwareScreen.kt: Observing Lifecycle Events in Compose

This file is where we listen to lifecycle changes like onResume, onPause, onStop, and onDestroy.

Here’s the full code with inline breakdowns:

Step 1: Create the Composable

@Composable
fun LifecycleAwareScreen(
    onResume: () -> Unit,
    onPause: () -> Unit,
    onStop: () -> Unit,
    onDestroy: () -> Unit
) {

This Composable takes 4 callbacks as parameters. Each one matches a lifecycle event we want to respond to.

Step 2: Get Lifecycle Owner

val lifecycleOwner = LocalLifecycleOwner.current

This gets access to the lifecycle of the current Activity or Composable. We’ll need it to attach an observer.

Step 3: Watch Lifecycle Events with DisposableEffect

DisposableEffect(lifecycleOwner) {
    val observer = LifecycleEventObserver { _, event ->
        when (event) {
            Lifecycle.Event.ON_RESUME -> onResume()
            Lifecycle.Event.ON_PAUSE -> onPause()
            Lifecycle.Event.ON_STOP -> onStop()
            Lifecycle.Event.ON_DESTROY -> onDestroy()
            else -> Unit
        }
    }

Here we:

  • Create a LifecycleEventObserver which is triggered anytime the lifecycle changes.

  • Call the appropriate lambda (e.g. onResume()) based on the event.

Step 4: Attach and Remove Observer

    lifecycleOwner.lifecycle.addObserver(observer)

    onDispose {
        lifecycleOwner.lifecycle.removeObserver(observer)
    }
}
  • We add the observer when the Composable enters the screen.

  • We remove the observer when the Composable is disposed to avoid memory leaks.

Step 5: UI Output

Text("Check Logcat for lifecycle logs.")

Just a simple message, but all the real work is happening behind the scenes through lifecycle callbacks.

By breaking down the code in MainActivity and LifecycleAwareScreen, we’ve seen how Jetpack Compose can integrate with the traditional Android lifecycle. Instead of relying on rigid, imperative lifecycle methods buried inside an activity, we now respond to lifecycle changes declaratively using DisposableEffect and LifecycleEventObserver. This structure not only keeps your code clean and testable but also makes resource management far more intuitive, ensuring sensors, media, and memory-heavy assets are only active when needed. The result? A smoother, leak-free app that respects both performance and user experience.

4. Running the App: What to Expect

Now that the code is in place, it's time to see the Android lifecycle in action. This section shows how to test your app’s behavior as it transitions through different lifecycle states, and how proper resource handling makes all the difference.

View Lifecycle Logs in Logcat

When you run the app on an emulator or real device, open Logcat in Android Studio and filter by:

Tag: LifecycleDemo

Each time a lifecycle event occurs — like onResume, onPause, onStop, etc. — you’ll see a log message. These logs help you understand when and how your code runs.

D/LifecycleDemo: onResume → Sensor ON + Media Play
D/LifecycleDemo: onPause → Sensor OFF + Media Pause
D/LifecycleDemo: onStop → Bitmap recycle
D/LifecycleDemo: onDestroy → Media release

You should see these printed depending on the interaction you're testing. Let’s explore what triggers them.

Try These Actions & Observe the Lifecycle

ActionLifecycle Events TriggeredWhat Should Happen
Launch the apponCreateonStartonResumeLoads image, prepares sensor, plays sound
Press HomeonPauseonStopSensor unregisters, media pauses, bitmap is recycled
Return to apponRestartonStartonResumeSensor re-registers, media resumes
Rotate screenonPauseonStoponDestroyonCreate → ...All resources are destroyed, then reinitialized
Swipe the app awayonPauseonStoponDestroyMediaPlayer and bitmap are released

Memory Behavior Before vs. After Cleanup

To really understand the value of proper lifecycle management, try this:

❌ Without Cleanup

Comment out the code in onPause(), onStop(), and onDestroy():

// Commenting this out for test purposes
// mediaPlayer?.pause()
// sensorManager.unregisterListener(this)
// largeBitmap?.recycle()
// mediaPlayer?.release()

Now run the app, rotate it a few times, switch apps, come back, and rotate again.

You may notice:

  • Increasing memory usage

  • Audio overlapping or not stopping

  • Sensors are continuing to run in the background

  • Eventually: OutOfMemoryError, lag, or crashes

Pro Tip: Monitor Memory Live

You can open the Profiler in Android Studio:

Go to View > Tool Windows > Profiler

  • Watch the memory graph while rotating the screen or switching between apps.

  • Without cleanup: memory keeps rising.

  • With cleanup: memory spikes then drops (recycled).

With proper cleanup in place, your app remains smooth, stable, and efficient, even as users rotate screens, background the app, or switch tasks. Without it, you’re headed for crashes, battery drain, and a poor user experience.

6. Best Practices for Resource Management in Compose

Jetpack Compose makes UI development feel like magic — no XML, no findViewById, and everything reacts to state changes.

But here’s the thing: Compose may handle UI recomposition for you, but it doesn't manage your media players, sensors, or bitmaps.

You’re still responsible for those, and that’s where these best practices come in.

Use DisposableEffect for Cleanup

Imagine you enter a room (a screen). You open a window (sensor), turn on a speaker (media), and light a candle (bitmap). Before you leave the room, you need to close the window, turn off the speaker, and blow out the candle — otherwise bad things happen.

That’s what DisposableEffect is for: it runs cleanup logic automatically when your composable leaves the screen or a key dependency changes.

Why it's essential:

  • Avoid memory leaks

  • Prevent background tasks from running forever

  • Reclaim memory when something is no longer visible

Avoid Holding Context-Sensitive Objects Across Recompositions

Some objects in Android rely on an active connection to the system. Think:

  • MediaPlayer

  • SensorManager

  • Bitmaps

  • Context from an activity

These objects can break or leak memory if you keep them alive after their screen or activity is gone.

🚫 Bad Example (for illustration):

val player = MediaPlayer.create(context, R.raw.sound)

If context is from a destroyed activity or screen; you just created a zombie — one that eats memory and never dies.

Better Way:

  • Use remember and DisposableEffect to scope their lifetime

  • Or even better, use ViewModel for non-UI state, which we’ll talk about next

Use ViewModel to Store State — Not Views or Context

Think of a ViewModel as a brain that survives configuration changes (like screen rotation). It stores data — but not UI elements or objects tied to an activity.

You use it to:

  • Hold state

  • Run business logic

  • Fetch or manage data

But never store a Context, Bitmap, or MediaPlayer Inside it. These are tied to the UI and must be cleaned up with the lifecycle.

Bad PracticeBetter Practice
Holding MediaPlayer across recompositionsUse DisposableEffect to release it
Keeping Context in a variableUse LocalContext only when needed, don't store
Doing cleanup manually in onPause()Use LifecycleObserver or DisposableEffect
Keeping everything in your ComposableUse ViewModel for long-lived state

7. Summary & Key Takeaways

Understanding the Android lifecycle is what separates glitchy, battery-hungry apps from polished, user-friendly ones. Whether it’s avoiding media playback in the background, shutting off sensors when not in use, or preventing memory leaks from forgotten Bitmaps, lifecycle awareness is essential. Just as users swipe away apps to free memory, developers must also know when to pause, release, or destroy resources. Jetpack Compose offers modern tools like remember, LaunchedEffect, and DisposableEffect to help you react cleanly to state changes — but they don’t eliminate the responsibility of cleanup. You still need to think carefully about when objects are created and when they should be released.

Releasing resources at the right time not only improves performance, it ensures your app remains stable and efficient. Use ViewModel for persistent data and let lifecycle hooks handle media players, sensors, and other heavy resources. The rule is simple: if you start something (like media or a sensor), you must stop it too. Done right, this will make your app faster ⚡, cleaner, and smarter . In short, Compose simplifies UI, but lifecycle discipline is still your job — and mastering it will make you a far more capable Android developer.

10
Subscribe to my newsletter

Read articles from Glory Olaifa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Glory Olaifa
Glory Olaifa

Glory Olaifa is an accomplished mobile engineer with a rich background spanning 4 years in the industry. He has demonstrated exceptional leadership skills, successfully guiding a team of 4 engineers in the development of a high-quality, production-ready application. A two-time hackathon winner and 2024 Google Solution winner, Glory is deeply passionate about community building. He recently spearheads the Google Developer Student Club at a prestigious university, where he actively nurtures a culture of learning and collaboration among students. In addition to his role at the university, Glory serves as the lead organizer for Flutter Ogbomosho, a position that highlights his dedication to fostering a vibrant developer community. His commitment to empowering others and his technical expertise make him a valuable asset to any team or project. Outside of his professional achievements, Glory enjoys playing football, video games, and sprinting. He also loves to party occasionally, embracing a well-rounded approach to life.