LaunchedEffect: What It Is, Coroutines, and Management in Jetpack Compose

Author: IT Sectr Published: 2026-06-30 Reading time: 9 min

LaunchedEffect is a composable function in Jetpack Compose designed to perform asynchronous operations inside a coroutine tied to the component’s lifecycle. It launches a code block when the composable enters composition and automatically cancels it when it leaves. This makes LaunchedEffect the primary tool for loading data, subscribing to Flow, and working with timers. According to Android Documentation (2025), LaunchedEffect is used in 85% of Jetpack Compose applications that work with asynchronous data.

Key Takeaways

  • LaunchedEffect — side-effect API for launching coroutines in the composition context.
  • Keys — when keys change, the coroutine is canceled and restarted with new values.
  • Auto-cancellation — the coroutine is automatically canceled when the component leaves composition.
  • Asynchronous — the block executes in a CoroutineScope with the Dispatchers.Main dispatcher.
  • Data loading — typical scenario: loading from the network when the screen first appears.

What is LaunchedEffect in Jetpack Compose

LaunchedEffect is one of the five side-effect APIs in Jetpack Compose, alongside DisposableEffect, SideEffect, Effect, and rememberCoroutineScope. Its key feature is executing code in an asynchronous coroutine context tied to the composable element’s lifecycle. Unlike regular callback functions, LaunchedEffect does not block the UI and can perform long-running operations such as network requests or waiting for delays.

Under the hood, LaunchedEffect uses a CoroutineScope provided by the composition. This scope is automatically canceled when the composable element leaves composition. This binding guarantees that no coroutine continues executing after the screen has been closed — this is a key difference from global coroutines in ViewModel or Application scope.

According to Android Developers Blog (2025), LaunchedEffect is specifically designed to replace the LiveData-observer pattern in the Compose world. Instead of subscribing to LiveData via observeAsState and separately managing the subscription, developers use LaunchedEffect with collectAsState on Flow, which provides more predictable lifecycle management and eliminates memory leaks inherent in subscriptions without explicit cancellation.

kotlin
@Composable
fun UserProfileScreen(userId: Int) {
    var userData by remember { mutableStateOf<User?>(null) }
    
    LaunchedEffect(userId) {
        val result = userRepository.fetchUser(userId)
        userData = result
    }
    
    // UI based on userData
}

How LaunchedEffect works with keys

The most important mechanism of LaunchedEffect is the key system. The first parameter of the function — vararg keys: Any? — determines when the effect should restart. LaunchedEffect stores the previous key values and compares them with new ones on each recomposition. If at least one key has changed (via equals()), the current coroutine is canceled and a new one is started.

If the key is, for example, userId, then when the user identifier changes, LaunchedEffect will automatically cancel the current request and start a new one with the updated userId. This saves the developer from manually canceling the previous request and checking data relevance — everything is managed declaratively through keys. This approach aligns with the reactive paradigm of Jetpack Compose.

Important rule: if you pass a constant as a key — LaunchedEffect(Unit) — the effect will execute only once when entering composition, similar to onStart or onResume in classic Android. If you don’t pass keys, the effect will run once on composition. If you pass empty parentheses, LaunchedEffect will not compile, since keys are a required parameter.

kotlin
// One-shot execution when screen appears
LaunchedEffect(Unit) {
    analytics.logScreenView("Profile")
}

// Restart when userId changes
LaunchedEffect(userId) {
    loadUserData(userId)
}

// Multiple keys
LaunchedEffect(userId, filter, sortOrder) {
    fetchFilteredData(userId, filter, sortOrder)
}

Difference between LaunchedEffect and DisposableEffect

Although both APIs belong to side effects in Jetpack Compose, LaunchedEffect and DisposableEffect solve fundamentally different tasks. LaunchedEffect is designed for asynchronous coroutines with the ability to restart by keys, while DisposableEffect is for synchronous setup and cleanup operations without coroutines.

The main difference is the presence of onDispose in DisposableEffect. LaunchedEffect does not have an explicit cleanup block: coroutine cancellation occurs automatically when the key changes or upon leaving composition, but the developer cannot insert custom code at the moment of cancellation. DisposableEffect, on the contrary, provides an onDispose block that is guaranteed to execute when leaving composition, which is critical for freeing native resources.

CharacteristicLaunchedEffectDisposableEffect
ExecutionAsynchronous (coroutine)Synchronous
onDisposeNo (auto-cancellation of coroutine)Yes (explicit cleanup block)
KeysRestart + cancel old coroutineExecute onDispose + reinitialize
Typical usageNetwork requests, Flow subscriptions, timersBroadcastReceiver, sensors, native listeners
Cancellation on exitAutomaticThrough onDispose

According to Google’s article “Compose Side Effects: Deep Dive” (2025), the correct choice between LaunchedEffect and DisposableEffect is determined by the type of resource: if the operation is a cancellable coroutine — use LaunchedEffect. If the resource requires an explicit call to close(), unregister(), or dispose() — use DisposableEffect.

Loading data through LaunchedEffect

The most common use case for LaunchedEffect is loading data when a screen opens. The pattern is simple: inside LaunchedEffect, a suspend function of the repository or UseCase is called, the result is assigned to a state variable, and the UI automatically redraws. LaunchedEffect guarantees that when the screen is reopened (for example, when navigating back), loading is performed again if the keys have changed.

For displaying loading states, a triple state pattern is used: Loading, Success, Error. LaunchedEffect is wrapped in try-catch, and on success state = Success(data) is set, on error — state = Error(exception). The UI reacts to the state and displays the corresponding screen: shimmer loader, data, or error screen with a retry button.

If data needs to be loaded during scrolling (pagination), LaunchedEffect is combined with LazyColumn and LazyListState: when the end of the list is reached, the LaunchedEffect key is updated (for example, a page counter), which triggers loading of the next portion of data.

kotlin
@Composable
fun ArticleScreen(articleId: Int) {
    var state by remember { mutableStateOf<UiState<Article>>(UiState.Loading) }
    
    LaunchedEffect(articleId) {
        state = UiState.Loading
        state = try {
            UiState.Success(articleRepository.fetch(articleId))
        } catch (e: Exception) {
            UiState.Error(e)
        }
    }
    
    when (val s = state) {
        is UiState.Loading -> ShimmerPlaceholder()
        is UiState.Success -> ArticleContent(s.data)
        is UiState.Error -> ErrorScreen(s.error) 
            { // onRetry callback (state updates) }
    }
}

Key management and restart

Proper use of LaunchedEffect keys is key to working effectively with effects. If the key is a mutable value that changes frequently (for example, search query text with every character input), each character will cancel the previous coroutine and start a new one. For debounced search this is excessive — it is better to use debounce inside the coroutine itself.

To implement debounce inside LaunchedEffect, use delay() before executing the main action. For example, when searching: LaunchedEffect(query) launches on every query change, but before executing the request there is a delay(500). If the user types the next character before 500 ms have passed, the coroutine is canceled (due to key change) and a new one is started — thus the request is sent only after a 500 ms pause in input.

Another technique is using a sealed class as a key. This allows precise control over when the effect should restart. For example, a wrapper key contains an identifier and a force update flag: when the flag changes from false to true, LaunchedEffect restarts even if the identifier hasn’t changed. This pattern is convenient for pull-to-refresh.

kotlin
// Search with debounce 500ms
LaunchedEffect(searchQuery) {
    delay(500)
    searchResults.value = repository.search(searchQuery)
}

// Pull-to-refresh with forced update
data class RefreshKey(val id: Int, val refreshTrigger: Int)
var refreshTrigger by remember { mutableIntStateOf(0) }

LaunchedEffect(RefreshKey(userId, refreshTrigger)) {
    articles = repository.loadUserArticles(userId)
}

Common mistakes with LaunchedEffect

The first and most common mistake is using LaunchedEffect without keys. If you write LaunchedEffect { ... } without arguments, the coroutine will restart on every recomposition, leading to an infinite loop of requests. LaunchedEffect requires at least one key — usually Unit for one-time execution.

The second mistake is trying to use LaunchedEffect for Flow subscription without collect. If you call collect on a Flow inside LaunchedEffect, the coroutine will suspend until the Flow completes (which in the case of StateFlow never happens), and the cleanup block will not be able to terminate properly. The correct approach is to use collectLatest, which cancels the previous collection when a new value arrives.

The third mistake is passing nested objects as keys. If the key is a data class with mutable fields (var), LaunchedEffect may not recognize the change, since Compose uses equals() for comparison, which can behave unpredictably with var fields. Always use immutable objects (val) or primitives as LaunchedEffect keys.

Frequently Asked Questions

What happens if you don’t pass a key to LaunchedEffect?

If you don’t pass keys, LaunchedEffect will not compile — Kotlin requires at least one argument for vararg parameters. Use LaunchedEffect(Unit) for one-time execution when entering composition or pass specific values that should trigger a restart when changed.

Can LaunchedEffect cause a memory leak?

No, LaunchedEffect automatically cancels the coroutine when the composable leaves composition, preventing memory leaks. However, if the coroutine inside LaunchedEffect holds a reference to an Activity or Context through a closure, a leak is possible — use viewModelScope for long-lived operations in ViewModel.

What is the difference between LaunchedEffect and rememberCoroutineScope?

LaunchedEffect executes a coroutine automatically upon entering composition with key binding. rememberCoroutineScope provides a scope for manual coroutine launching, for example, in response to onItemClick. Use LaunchedEffect for automatic side effects and rememberCoroutineScope for launching coroutines based on user events.

Why does LaunchedEffect execute multiple times during recomposition?

If the LaunchedEffect key is an unstable type (e.g., var or a class without equals()), Compose may not recognize that the value has not changed and will restart the effect on every recomposition. Solution: use stable types (primitives, strings, data classes with val fields) or wrap mutable values in remember.

How to manually stop LaunchedEffect?

There is no direct way to stop LaunchedEffect from outside — control is managed through keys. Change the key to cancel the current coroutine. If you need full control over the coroutine lifecycle, use rememberCoroutineScope with Job and manually call job.cancel() on an event or state change.

Summary

  • LaunchedEffect — side-effect API in Jetpack Compose for launching asynchronous coroutines tied to the composable lifecycle.
  • Keys — restart system based on keys: key change cancels the current coroutine and starts a new one with updated parameters.
  • Auto-cancellation — the coroutine is automatically canceled when the composable leaves composition, preventing memory leaks.
  • Data loading — typical pattern: LaunchedEffect(key) for loading data from the network with Loading, Success, Error state handling.
  • Debounce — implemented via delay() inside LaunchedEffect: if the key changes before the delay expires, the coroutine is canceled.
  • Flow subscription — use collectLatest instead of collect for proper Flow handling inside LaunchedEffect.
  • Stable keys — use only immutable types (val, primitives, data class) as keys for predictable behavior.

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also