Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2024 The Android Open Source Project
Comment thread
sudeshim3 marked this conversation as resolved.
Outdated
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.compose.snippets.performance

import android.annotation.SuppressLint
import android.location.LocationListener
import android.location.LocationManager
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

// Pattern 3: Example 1 - Compose registers a system service listener without cleanup

@SuppressLint("MissingPermission")
// [START android_compose_performance_memory_leak_location_with_leak]
@Composable
fun LocationScreenWithLeak(locationManager: LocationManager) {
var locationText by remember { mutableStateOf("Locating...") }

// This registers the listener when entering composition, but leaves it attached to the OS when leaving
LaunchedEffect(locationManager) {
locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L, 1f) { location ->
locationText = "Lat: ${location.latitude}, Lng: ${location.longitude}"
}
}

Text(text = locationText)
}
// [END android_compose_performance_memory_leak_location_with_leak]

@SuppressLint("MissingPermission")
// [START android_compose_performance_memory_leak_location_recommended]
@Composable
fun LocationScreenRecommended(locationManager: LocationManager) {
var locationText by remember { mutableStateOf("Locating...") }
Copy link
Copy Markdown
Contributor

@raystatic raystatic May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is recommended to use rememberUpdatedState for long-live operations.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sticking to the base remember keeps the example easy to read while still being technically correct. My goal was just to show the setup/teardown mechanism without getting bogged down in advanced Compose state mechanics. It will shift the the readers focus from 'how to clean up resources' to 'how Compose handles stale closures in lambdas'


// DisposableEffect provides an onDispose block for mandatory cleanup
DisposableEffect(locationManager) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, please check LifecycleStartEffect

It shows a location manager example only.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are many platform-specific lifecycle handlers that developers should adopt based on their exact requirements. the primary focus of this document is on general memory leak patterns and bringing an understanding of how failing to clean up a UI component causes a leak. Introducing specific lifecycle APIs brings the focus to the example(Location Based app) instead of communicating the concept.
For example: LifecycleStartEffect aggressively tears down on onStop (when the app goes to the background), whereas DisposableEffect keeps a connection alive as long as the component is still in the UI tree. Whether you do it when the app goes to the background, or when your component is getting destroyed, is left for the developer to explore based on their exact needs.

val listener = LocationListener { location ->
locationText = "Lat: ${location.latitude}, Lng: ${location.longitude}"
}

locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000L, 1f, listener)

// Automatically executed when this Composable leaves the screen
onDispose {
locationManager.removeUpdates(listener)
}
}

Text(text = locationText)
}
// [END android_compose_performance_memory_leak_location_recommended]
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.snippets.memoryleaks

import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch

// Mock classes for snippets to compile
class User(val name: String)
class Api {
fun getUser(callback: (User) -> Unit) {}
suspend fun getUser(): User = User("Mock")
}
val api = Api()

class UserFragmentBinding {
val root: View? = null
val name: TextView? = null
companion object {
fun bind(view: View): UserFragmentBinding = UserFragmentBinding()
fun inflate(inflater: LayoutInflater, container: ViewGroup?, b: Boolean): UserFragmentBinding = UserFragmentBinding()
}
}
val binding = UserFragmentBinding()
annotation class Singleton
annotation class Inject
annotation class ApplicationContext
class ActivityImagePicker(val activity: Activity)

// Pattern 1: Example 1 - Repository retains a UI callback

// [START android_memory_leak_repository_callback_with_leak]
class UserRepositoryWithLeak {
private var listener: ((User) -> Unit)? = null

fun fetchUser(callback: (User) -> Unit) {
// The repository retains the callback beyond the view lifecycle.
listener = callback
api.getUser { user ->
listener?.invoke(user)
}
}
}

class UserFragmentCallbackWithLeak : Fragment() {
private val repository = UserRepositoryWithLeak()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = UserFragmentBinding.bind(view)

// The Fragment passes a callback that retains a reference to the binding.
// The repository holds this callback even after the Fragment's view is destroyed.
repository.fetchUser { user ->
binding.name?.text = user.name
}
}
}
// [END android_memory_leak_repository_callback_with_leak]

// [START android_memory_leak_repository_callback_recommended_pt1]
class UserRepositoryRecommended {
suspend fun fetchUser(): User {
return api.getUser()
}
}
// [END android_memory_leak_repository_callback_recommended_pt1]

class UserViewModel(private val repository: UserRepositoryRecommended) : ViewModel() {
private val _userState = MutableStateFlow<User?>(null)
val userState: StateFlow<User?> = _userState.asStateFlow()

// [START android_memory_leak_repository_callback_recommended_pt2]
fun loadUser() {
// viewModelScope automatically cancels if the user leaves the screen
viewModelScope.launch {
val user = repository.fetchUser()
_userState.value = user
}
}
// [END android_memory_leak_repository_callback_recommended_pt2]
}

val viewModel = UserViewModel(UserRepositoryRecommended())

// Pattern 1: Example 2 - Singleton depends on a UI-scoped object

// [START android_memory_leak_singleton_dependency_with_leak]
@Singleton
class ImageLoaderWithLeak @Inject constructor(
private val activityImagePicker: ActivityImagePicker
)

class ActivityImagePickerWithLeak @Inject constructor(
// Injecting Activity here makes this dependency activity-scoped.
private val activity: Activity
)
// [END android_memory_leak_singleton_dependency_with_leak]

// [START android_memory_leak_singleton_dependency_recommended]
// Option 1: Pass the Activity dynamically for UI-scoped tasks (like image picking)
@Singleton
class ImagePickerRecommended @Inject constructor() {
fun pickImage(activity: Activity) { /* ... */ }
}

// Option 2: Inject Application Context for non-UI/background tasks (like disk caching or sharedPreferences)
@Singleton
class ImageCacheRecommended @Inject constructor(
@ApplicationContext private val context: Context
)
// [END android_memory_leak_singleton_dependency_recommended]

// Pattern 2: Example 1 - Fragment collects Flow with the incorrect lifecycle

// [START android_memory_leak_fragment_flow_with_leak]
class UserFragmentWithLeak : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = UserFragmentBinding.bind(view)

lifecycleScope.launch {
// This coroutine is tied to the fragment lifecycle, not the view lifecycle.
viewModel.userState.collect { user ->
binding.name?.text = user?.name
}
}
}
}
// [END android_memory_leak_fragment_flow_with_leak]

// [START android_memory_leak_fragment_flow_recommended]
class UserFragmentRecommended : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val binding = UserFragmentBinding.bind(view)

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userState.collect { user ->
binding.name?.text = user?.name
}
}
}
}
}
// [END android_memory_leak_fragment_flow_recommended]

// Pattern 2: Example 2 - Delayed work captures an Activity

// [START android_memory_leak_delayed_work_with_leak]
// Singleton scope accepts a UI-bound callback
object UserRepositoryDelayedWithLeak {
private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)

// Accepts a callback that might capture a destroyed UI Context
fun fetchUserData(onComplete: (String) -> Unit) {
repositoryScope.launch {
// network or database call that suspends
val user = api.getUser()
// If the Activity was destroyed while waiting for the API,
// invoking this callback will leak the Activity!
onComplete(user.name)
}
}
}

class MainActivityWithLeak : AppCompatActivity() {
Comment thread
sudeshim3 marked this conversation as resolved.
Outdated
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// The trailing lambda implicitly captures 'this' (MainActivity) to update the title
UserRepositoryDelayedWithLeak.fetchUserData { data ->
title = data
}
}
}
// [END android_memory_leak_delayed_work_with_leak]

// [START android_memory_leak_delayed_work_recommended]
// Expose data as a Flow and let the UI handle the lifecycle scope
object UserRepositoryDelayedRecommended {
// A clean, stateless flow with no callback parameters
fun getUserData(): Flow<String> = flow {
val user = api.getUser()
emit(user.name)
}
}

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

// Automatically cancels collection and releases MainActivity when destroyed
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
UserRepositoryDelayedRecommended.getUserData().collect { data ->
title = data
}
}
}
Comment thread
sudeshim3 marked this conversation as resolved.
}
}
// [END android_memory_leak_delayed_work_recommended]
Loading