Sitemap

Fixing LaunchedEffect Triggering Before a Screen is Visible in Jetpack Compose

3 min readMay 26, 2025

Have you ever seen your API calls run multiple times when you switch between screens in Jetpack Compose? Yeah, same here. 😅 At first, I thought there was a some issue in my code. But after going deeper, I found out it’s just how Compose works with navigation.

Let me share what happened and how I finally fixed it.

What Was Happening in My App?

So I was building this simple tracking app. just two main screens, which can be navigated using the pager state

  • A Streak Calendar
  • And a Consistency Rate section

Each screen had its own LaunchedEffect to fetch data from the ViewModel:

// Screen 1
@Composable
fun StreakCalendar() {
LaunchedEffect(true) {
viewModel.getStreakData()
viewModel.geTrackerChartData(4)
burpyLog("TrackerChartData", "Called from StreakCalendar")
}
}
// Screen 2
@Composable
fun ConsistencyRateSection() {
val selectedIndex = remember { mutableIntStateOf(0) }

LaunchedEffect(selectedIndex.value) {
burpyLog("TrackerChartData", "Called from ConsistencyRateSection")
trackersViewModel.getrTrackerChartData(selectedIndex.value)
}
}

I expected each screen to load data just once — when the user visits it.

But what actually happened?

Called from water trend section with index: 0
Called from ConsistencyRateSection
Called from FullStreakCalendar LaunchedEffect
Called from ConsistencyRateSection
Called from FullStreakCalendar LaunchedEffect
...

Yeah… it was a mess. API calls were firing even when I wasn’t on that screen.

🤔 So… What’s Going On?

This isn’t a bug — it’s actually expected behavior in Jetpack Compose.

🔄 Both Screens Are Temporarily Active

When you navigate from one screen to another, Jetpack Compose keeps both the old and new screens in the composition for a short time. This helps make transitions smooth (like fade-in/out animations).

But here’s the catch:
If both screens have a LaunchedEffect, they’ll both run — which means 2 API calls. 🔥

♻️ Recomposition Can Also Trigger Effects

Even after navigating away, the previous screen might still recompose for a moment. When that happens, LaunchedEffect might run again, even though you're no longer on that screen.

So What’s the Result?

  • Two screens are alive during the transition
  • Recomposition
  • LaunchedEffect gets triggered multiple times

🎯 End result? Unwanted and unnecessary API calls. Not cool for performance!

The Simple Solution: OnResumeEffect

After trying various lifecycle-aware approaches, I found the cleanest solution. Instead of fighting with LaunchedEffect, we need an effect that only triggers when a screen is actually resumed and visible to the user.

Step 1: Create the Custom Composable

@Composable
fun OnResumeEffect(
vararg keys: Any?,
block: suspend CoroutineScope.() -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()

DisposableEffect(lifecycleOwner, *keys) {
val observer = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
coroutineScope.launch {
block()
}
}
}

lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
}

or

@Composable
fun DebouncedLifecycleEffect(
vararg keys: Any?,
delayMs: Long = 100L,
block: suspend CoroutineScope.() -> Unit
) {
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()

LaunchedEffect(*keys, lifecycleState) {
if (lifecycleState.isAtLeast(Lifecycle.State.RESUMED)) {
delay(delayMs) // Small delay to debounce rapid state changes
if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
block()
}
}
}
}

Step 2: Replace LaunchedEffect with OnResumeEffect

// Before ❌
@Composable
fun ConsistencyRateSection() {
LaunchedEffect(selectedIndex.value) {
trackersViewModel.getTrackerChartData(selectedIndex.value)
}
}
// After ✅
@Composable
fun ConsistencyRateSection() {
OnResumeEffect(selectedIndex.value) {
trackersViewModel.getTrackerChartData(selectedIndex.value)
}
}

Why This Works So Well

  • Only triggers on actual resume events — not during composition overlaps
  • Uses lifecycle observers directly — no state collection timing issues
  • Automatically cleans up — removes observers when composable is disposed
  • Respects navigation lifecycle — only executes when screen is truly active

Use OnResumeEffect when:

  • Making API calls that should only happen when screen is visible
  • Starting location updates, sensors, or other resources
  • Analytics tracking for screen views
  • Any side effect that should respect the actual user’s current screen

Conclusion

If you’ve ever seen duplicate API calls during screen changes in Jetpack Compose and felt like you’re losing your mind — you’re not alone! This is one of those tricky “gotcha” moments.

But here’s the truth:
It’s not a bug — it’s just how Compose works. During screen transitions, Compose keeps both the old and new screens active for a short time. That overlap is what causes LaunchedEffect to run more than once.

Hope this helped you :)
May be you can like this post too 🙂

Thankyou,
Happy Coding

--

--

Rohit kumar
Rohit kumar

Responses (1)