Avoiding Memory Leaks and Lag: The Android Lifecycle Demystified

Table of contents
- 1. 🚧 Understanding the Android Lifecycle (with Compose in Mind)
- 2. Step-by-Step Code Breakdown
- MainActivity.kt: Managing Heavy Resources
- LifecycleAwareScreen.kt: Observing Lifecycle Events in Compose
- 4. Running the App: What to Expect
- 6. Best Practices for Resource Management in Compose
- 7. Summary & Key Takeaways

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
Method | When It Happens | What You Should Do Here |
onCreate() | When the activity is first created | Setup things: UI, state, sensors, media player |
onStart() | Just before the activity becomes visible | Start lightweight tasks like tracking, analytics |
onResume() | Right when the activity is interactive | Start 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 visible | Release big stuff like bitmaps or large objects |
onDestroy() | Activity is about to be destroyed permanently | Clean 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:
Line | Purpose |
sensorManager | Gets the Android system’s sensor service |
accelerometer | Gets 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 usingDisposableEffect
.
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
Action | Lifecycle Events Triggered | What Should Happen |
Launch the app | onCreate → onStart → onResume | Loads image, prepares sensor, plays sound |
Press Home | onPause → onStop | Sensor unregisters, media pauses, bitmap is recycled |
Return to app | onRestart → onStart → onResume | Sensor re-registers, media resumes |
Rotate screen | onPause → onStop → onDestroy → onCreate → ... | All resources are destroyed, then reinitialized |
Swipe the app away | onPause → onStop → onDestroy | MediaPlayer 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
andDisposableEffect
to scope their lifetimeOr 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 Practice | Better Practice |
Holding MediaPlayer across recompositions | Use DisposableEffect to release it |
Keeping Context in a variable | Use LocalContext only when needed, don't store |
Doing cleanup manually in onPause() | Use LifecycleObserver or DisposableEffect |
Keeping everything in your Composable | Use 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.
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.