๐ง When Can Memory Leaks Happen?
Memory leaks in Android typically happen when an object that holds a reference to a Context (especially Activity) outlives its lifecycle, preventing the system from garbage collecting the Activity.
๐งฉ Types of Context and When to Use Which
โ ๐ซ Avoid Memory Leaks with These Tips
โ 1. Use applicationContext when:
You donโt need to access UI
You're working in long-lived classes: e.g., repositories, databases, analytics trackers
val context = context.applicationContext
โ 2. Avoid Anonymous Inner Classes in Activities
Use static classes or top-level classes to avoid holding implicit references to Activities.
โ 3. Clear Resources in Lifecycle
Always clear:
Observers (LiveData, BroadcastReceiver)
Coroutine scopes (Job.cancel())
Ad SDKs (nativeAd.destroy())
โ 4. Donโt Hold Context in Singletons
If you must, store applicationContext, not Activity.
class MySingleton private constructor(private val context: Context) {
companion object {
fun init(appContext: Context): MySingleton {
return MySingleton(appContext.applicationContext)
}
}
}
- Holding a reference to an Activity or View in a Singleton โ Problem:
object MyManager {
var context: Context? = null // holding activity context
}
๐ Real-World Example:
You create a singleton for analytics or ads that stores an Activity context to show a Toast or dialog โ but forget to clear it.
โ Solution:
Always use context.applicationContext in singletons.
context = context.applicationContext
- Inner classes or anonymous classes (e.g. Runnable, Listener) holding implicit reference to outer Activity
โ Problem:
class MyActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper())
fun start() {
handler.postDelayed({
// 'this' refers to Activity, leak if Activity is destroyed
}, 10000)
}
}
โ Solution:
Make the runnable a static class or cancel delayed tasks in onDestroy().
override fun onDestroy() {
super.onDestroy()
handler.removeCallbacksAndMessages(null)
}
- Long-running background tasks (Coroutines, AsyncTask, Threads) holding context
โ Problem:
You start a coroutine in a ViewModel or plain class and hold an Activity context.
fun loadData(context: Context) {
CoroutineScope(Dispatchers.IO).launch {
// Holding context across configuration change or after activity is gone
val db = DB.getInstance(context)
}
}
โ Solution:
- Pass applicationContext if needed.
- Cancel coroutine on lifecycle.
fun loadData(appContext: Context) {
CoroutineScope(Dispatchers.IO).launch {
val db = DB.getInstance(appContext)
}
}
- LiveData or Flow observing with lifecycle issues
โ Problem:
You observe LiveData from ViewModel in a Context-based class (like a custom manager), not tied to lifecycle.
viewModel.data.observeForever {
// Forever means it never stops -> leak
}
โ Solution:
Always observe with LifecycleOwner (Activity or Fragment) unless you manually remove observers.
viewModel.data.observe(viewLifecycleOwner) { ... }
- Dialogs or Toasts shown after Activity is destroyed โ Problem:
fun showDialog(context: Context) {
AlertDialog.Builder(context)
.setMessage("Hello")
.show()
}
If context is an Activity and itโs already finishing, the window leaks.
โ
Solution:
Check context:
if (context is Activity && !context.isFinishing) {
AlertDialog.Builder(context)
.setMessage("Safe")
.show()
}
- Static Views or holding binding in fragments after view is destroyed โ Problem:
class MyFragment : Fragment() {
private lateinit var binding: FragmentMyBinding
override fun onDestroyView() {
super.onDestroyView()
// Forget to clear binding -> view leak
}
}
โ Solution:
Clear binding in onDestroyView():
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- Custom callbacks or listeners not removed โ Problem:
someView.setOnClickListener {
// Holds reference to outer Activity
}
If the view lives longer (e.g., part of an SDK), it keeps the activity alive.
โ
Solution:
Remove listeners:
override fun onDestroy() {
someView.setOnClickListener(null)
}
class MySingleton private constructor(private val context: Context) {
companion object {
@Volatile
private var INSTANCE: MySingleton? = null
fun getInstance(appContext: Context): MySingleton {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: MySingleton(appContext.applicationContext).also { INSTANCE = it }
}
}
}
// Example usage of application context
fun doSomethingGlobal() {
Toast.makeText(context, "Doing something!", Toast.LENGTH_SHORT).show()
}
}
private var weakActivity: WeakReference<Activity>? = null
fun bindActivity(activity: Activity) {
weakActivity = WeakReference(activity)
}
fun doSomething() {
val activity = weakActivity?.get() ?: return
// Use it safely
}