🧠 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

Image description

✅ 🚫 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)
        }
    }
}
  1. 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
  1. 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)
}
  1. 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)
    }
}
  1. 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) { ... }
  1. 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()
}
  1. 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
}
  1. 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
}