Memory problems rarely show up as crashes in your testing — they show up as slow degradation, background kills, and one-star reviews mentioning "app gets laggy after a while." In high-traffic fintech apps handling real-time data, getting this wrong is expensive. Here's what's actually worked in production.
Before optimizing anything, profile. Android Studio's Memory Profiler shows live heap allocation, and the Layout Inspector catches view hierarchy bloat. For leak detection specifically, LeakCanary is worth keeping in debug builds permanently — it catches Activity and Fragment leaks automatically and gives you a reference chain, which saves hours of manual heap dump analysis.
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.x")
Most leaks in production Android code trace back to a small set of patterns:
Activity or Context — common in singletons or companion objects that outlive the screen.In Compose specifically, forgetting an onDispose block inside DisposableEffect is the most common way I've seen listeners leak.
If a class needs a Context just to access resources, system services, or shared preferences — not anything UI-related — pass the application context instead of an Activity context. It can't leak an Activity because it isn't tied to one.
class AnalyticsLogger(private val appContext: Context) {
// appContext = context.applicationContext, not the Activity
}
Images are usually the single biggest memory consumer in any app with a feed, gallery, or chart-heavy UI. A few rules that consistently help:
inSampleSize if you're handling decoding yourself.Long-running background work is a common silent leak source — a job holding a reference to a ViewModel or Activity well after the screen is gone. Tie coroutine scopes to viewModelScope or lifecycleScope rather than a custom scope you have to remember to cancel manually, and for true background work (sync, uploads), use WorkManager so the OS manages the lifecycle instead of your app holding a wakelock or thread alive indefinitely.
In real-time apps — order books, live feeds, chat — unbounded in-memory caches are an easy way to slowly grow your heap over a session. A few practices that helped on a trading app with constant WebSocket updates:
LruCache for anything that's a cache by definition rather than a plain HashMap.A flow that performs fine on a high-RAM device can trigger frequent background process kills on a budget device with 2-3GB RAM. If your user base spans price points, profile on the cheapest device in your target range, not just your dev phone — Android's low-memory killer behaves very differently under real memory pressure than it does in a profiler running on a powerful test device.