Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AmolPardeshi99/android-performance-skills/llms.txt
Use this file to discover all available pages before exploring further.
Any system service, library, or custom manager that stores a listener holds a reference to the registering object. Without symmetric unregistration at the correct lifecycle boundary, the Fragment or Activity is pinned in memory for the duration of the component that holds the listener.
1.6 Listeners, observers, and BroadcastReceivers
SensorManager
// ❌ LEAK: sensor listener never removed
class CompassFragment : Fragment() {
private val sensorManager by lazy { requireContext().getSystemService(SensorManager::class.java)!! }
private val sensor by lazy { sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) }
private val listener = object : SensorEventListener {
override fun onSensorChanged(e: SensorEvent) { updateCompass(e.values) }
override fun onAccuracyChanged(s: Sensor, a: Int) {}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI)
// No matching unregister → SensorManager holds CompassFragment reference
}
}
// ✅ CORRECT: symmetric pairing
class CompassFragment : Fragment() {
override fun onResume() { super.onResume(); sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_UI) }
override fun onPause() { super.onPause(); sensorManager.unregisterListener(listener) }
}
BroadcastReceiver
// ❌ LEAK: BroadcastReceiver registered without unregister
// Also: CONNECTIVITY_ACTION is deprecated since API 28 — use NetworkCallback instead
class NetworkFragment : Fragment() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { checkNetwork() }
}
override fun onStart() {
super.onStart()
@Suppress("DEPRECATION")
requireContext().registerReceiver(receiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
// No unregister → receiver holds Fragment reference
}
}
// ✅ CORRECT unregister pairing (if BroadcastReceiver is still needed):
override fun onStop() { super.onStop(); requireContext().unregisterReceiver(receiver) }
NetworkCallback (preferred over CONNECTIVITY_ACTION, API 28+)
CONNECTIVITY_ACTION is deprecated as of API 28. Use ConnectivityManager.NetworkCallback instead — no BroadcastReceiver, no unregister leak risk.
// ✅ PREFERRED (API 28+): NetworkCallback
class NetworkFragment : Fragment() {
private val connectivityManager by lazy {
requireContext().getSystemService(ConnectivityManager::class.java)
}
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { checkNetwork(available = true) }
override fun onLost(network: Network) { checkNetwork(available = false) }
}
override fun onStart() {
super.onStart()
connectivityManager.registerDefaultNetworkCallback(networkCallback)
}
override fun onStop() {
super.onStop()
connectivityManager.unregisterNetworkCallback(networkCallback)
}
}
Lifecycle pairing table
| Register in | Unregister in |
|---|
onCreate | onDestroy |
onStart | onStop |
onResume | onPause |
onCreateView / onViewCreated | onDestroyView |
LiveData — this vs viewLifecycleOwner
In Fragments, always pass viewLifecycleOwner — not this — as the LifecycleOwner to observe(). Using this means the observer is tied to the Fragment’s lifecycle, not the View’s. When the user navigates away and back, the View is recreated but the Fragment is not destroyed, so a second observer is added. The result is duplicate callbacks and the Fragment’s reference being held by the old observer indefinitely.
// ❌ LEAK: using Fragment 'this' as observer — observer never removed when View is destroyed
// After navigating away and back, a second observer is added → duplicate callbacks
viewModel.data.observe(this) { render(it) }
// ✅ CORRECT: viewLifecycleOwner is destroyed with the View
viewModel.data.observe(viewLifecycleOwner) { render(it) }
Flow — repeatOnLifecycle vs bare launch
Always collect Flow in a Fragment using viewLifecycleOwner.lifecycleScope.launch combined with repeatOnLifecycle(Lifecycle.State.STARTED). This suspends collection when the app is backgrounded and resumes it when the app returns to the foreground, preventing both CPU waste and potential leaks from coroutines that outlive the visible lifecycle.
// ✅ CORRECT for Flow — repeatOnLifecycle suspends collection in background, resumes in foreground
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { render(it) }
}
}
// ❌ BAD: bare launch collects even when app is backgrounded — wastes CPU and may leak
// lifecycleScope.launch { viewModel.uiState.collect { render(it) } }
1.7 ViewModel holding UI references
ViewModel survives configuration changes. Anything stored in it lives at least until the owner Activity or NavGraph is permanently finished.
// ❌ LEAK: Activity, View, or Binding stored in ViewModel
class DashboardViewModel : ViewModel() {
var activity: Activity? = null // leaks across rotation
var chart: ChartView? = null // leaks View tree
lateinit var binding: ActivityDashboardBinding // leaks Context
}
// ❌ LEAK: lambda from Activity stored in Repository singleton → ViewModel retained
// The lambda captures ViewModel implicitly, Repository (singleton) holds the lambda,
// so ViewModel lives forever
class OrderRepository {
private var onComplete: (() -> Unit)? = null
fun setCallback(cb: () -> Unit) { onComplete = cb }
// Caller must call clearCallback() or ViewModel leaks
}
class OrderViewModel(private val repo: OrderRepository) : ViewModel() {
init { repo.setCallback { _uiState.value = UiState.Done } }
override fun onCleared() { super.onCleared(); repo.clearCallback() } // REQUIRED
}
// ✅ CORRECT: ViewModels contain only application-scoped objects
class DashboardViewModel(
private val dashboardRepo: DashboardRepository, // no Android framework types
@ApplicationContext private val context: Context // applicationContext is fine
) : ViewModel() {
val stats: StateFlow<Stats> = dashboardRepo.stats
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), Stats.Empty)
}
1.8 Coroutine scope and GlobalScope leaks
Root cause: A coroutine captures every variable it references from its outer scope, including binding (View) and this (Activity / Fragment). If the coroutine runs on a scope that outlives the screen, all those references are retained.
// ❌ LEAK: GlobalScope — no lifecycle, captures binding, runs until app death
class OrderFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
GlobalScope.launch {
val order = api.fetchOrder()
withContext(Dispatchers.Main) {
binding.total.text = order.total // binding captured; Fragment is gone
}
}
}
}
// ❌ LEAK: manual CoroutineScope not cancelled on destroy
class OrderFragment : Fragment() {
private val scope = CoroutineScope(Dispatchers.Main)
// Missing: override fun onDestroyView() { scope.cancel() }
}
// ✅ CORRECT: lifecycle-bound scopes
class OrderFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// viewLifecycleOwner.lifecycleScope is cancelled in onDestroyView automatically
viewLifecycleOwner.lifecycleScope.launch {
val order = withContext(Dispatchers.IO) { api.fetchOrder() }
binding.total.text = order.total
}
}
}
// ✅ In ViewModel: viewModelScope cancelled in onCleared()
class OrderViewModel(private val api: OrderApi) : ViewModel() {
fun loadOrder(id: String) {
viewModelScope.launch {
val order = withContext(Dispatchers.IO) { api.fetchOrder(id) }
_uiState.value = UiState.Success(order)
}
}
}
// ✅ Application-level background work: inject a custom application-scoped CoroutineScope
// (not GlobalScope) via DI
@Singleton
class AppCoroutineScope @Inject constructor() : CoroutineScope by CoroutineScope(
SupervisorJob() + Dispatchers.Default
)