-
Notifications
You must be signed in to change notification settings - Fork 365
Add memory leak examples for Android documentation #920
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
8e3a6cc
e240bff
535b120
6058047
7319644
23d97a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /* | ||
| * 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.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...") } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is recommended to use rememberUpdatedState for long-live operations.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, please check LifecycleStartEffect It shows a location manager example only.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| 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() { | ||
|
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 | ||
| } | ||
| } | ||
| } | ||
|
sudeshim3 marked this conversation as resolved.
|
||
| } | ||
| } | ||
| // [END android_memory_leak_delayed_work_recommended] | ||
Uh oh!
There was an error while loading. Please reload this page.