๐Ÿง  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
}