DEV Community

Cover image for Day 6/100: Context in Android — The Wrong One Will Leak Your Entire Activity
Hoang Son
Hoang Son

Posted on

Day 6/100: Context in Android — The Wrong One Will Leak Your Entire Activity

This is Day 6 of my 100 Days to Senior Android Engineer series. Each post: what I thought I knew → what I actually learned → interview implications.


🔍 The concept

Context is one of those Android classes you use dozens of times per day without thinking about it. You pass it to constructors, use it to inflate views, start activities, access resources.

But Context isn't one thing — it's a family of related objects with very different lifetimes. Pass the wrong one into a long-lived object, and you've just anchored that object to a screen that the user may have left minutes ago. The screen can't be garbage collected. You've created a memory leak.

Not a theoretical one. A real one that affects every user who navigates back to that screen.


💡 What I thought I knew

My rule of thumb used to be: "Use applicationContext when in doubt."

That's not wrong exactly — applicationContext won't leak. But it's incomplete, and applying it blindly causes a different class of bugs: UI operations that crash, theme-aware resources that return wrong values, dialogs that fail to show.

The full picture is more nuanced, and more interesting.


😳 What I actually learned

What Context actually is

Context is an abstract class that provides access to application-level resources and operations:

Context (abstract)
  ├── ContextWrapper
  │     ├── Application        ← lives as long as the process
  │     ├── Activity           ← lives as long as the screen
  │     └── Service            ← lives as long as the service
  └── ContextImpl              ← the real implementation, hidden from you
Enter fullscreen mode Exit fullscreen mode

Every Activity is a Context. Every Application is a Context. They wrap the same underlying ContextImpl but expose different capabilities and, critically, have different lifetimes.


The lifetime mismatch that causes leaks

The classic pattern that causes a leak:

// ❌ Singleton holding an Activity Context
object ImageLoader {
    private lateinit var context: Context

    fun init(context: Context) {
        // If caller passes an Activity, this singleton now holds
        // a reference to that Activity for the lifetime of the process
        this.context = context
    }
}

// In Activity:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ImageLoader.init(this)  // 💀 leaking 'this' into a singleton
}
Enter fullscreen mode Exit fullscreen mode

The singleton lives forever. It holds the Activity. The Activity holds its entire view hierarchy — every View, every Bitmap, every Drawable. None of it can be garbage collected.

Rotate the screen twice and you have three leaked Activity instances, each holding their full view hierarchy in memory.

The fix is one word:

fun init(context: Context) {
    this.context = context.applicationContext  // ✅ survives rotation, no leak
}
Enter fullscreen mode Exit fullscreen mode

applicationContext has the same lifetime as the process. No Activity is held. No leak.


But applicationContext isn't always correct

Here's where my old rule of thumb breaks down. applicationContext doesn't have a theme. It doesn't know which Activity it's in. For operations that depend on the UI context, it either crashes or silently returns wrong results.

// ❌ Inflating a view with applicationContext — theme attributes broken
val view = LayoutInflater.from(applicationContext).inflate(R.layout.my_view, null)
// Colors from ?attr/colorPrimary, ?attr/textAppearanceBody1, etc. — all wrong

// ✅ Inflate with Activity context — theme attributes resolved correctly
val view = LayoutInflater.from(this).inflate(R.layout.my_view, null)
Enter fullscreen mode Exit fullscreen mode
// ❌ Showing a Dialog with applicationContext — crashes on API 23+
AlertDialog.Builder(applicationContext)  // throws WindowManager$BadTokenException
    .setMessage("Are you sure?")
    .show()

// ✅ Show Dialog with Activity context
AlertDialog.Builder(this)
    .setMessage("Are you sure?")
    .show()
Enter fullscreen mode Exit fullscreen mode
// ❌ Starting an Activity from applicationContext without NEW_TASK flag — crashes
applicationContext.startActivity(Intent(applicationContext, DetailActivity::class.java))
// throws AndroidRuntimeException: Calling startActivity() from outside of an Activity context

// ✅ With the required flag
applicationContext.startActivity(
    Intent(applicationContext, DetailActivity::class.java).apply {
        flags = Intent.FLAG_ACTIVITY_NEW_TASK
    }
)
Enter fullscreen mode Exit fullscreen mode

The decision table

After going through the docs carefully, this is the table I now keep in my head:

Operation Application Context Activity Context
Start an Activity ⚠️ needs NEW_TASK flag
Start a Service
Send a Broadcast
Load a resource (string, drawable)
Inflate a layout with theme ❌ wrong theme
Show a Dialog ❌ crashes
Create a ViewModel
Initialize a singleton / library ❌ leaks
Access SharedPreferences
Get system services (e.g. ClipboardManager)

The pattern: anything UI-related needs Activity context. Anything that lives longer than a screen needs Application context.


The this vs requireContext() vs requireActivity() confusion in Fragments

Fragments add another layer of confusion because they're not a Context themselves — they have a context.

class MyFragment : Fragment() {

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

        // 'this' — the Fragment, NOT a Context
        // Can't be passed where Context is expected

        // requireContext() — the Activity context (or throws if detached)
        val prefs = requireContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)

        // requireActivity() — explicitly the Activity, useful when you need
        // Activity-specific APIs like ViewModelProvider, window insets, etc.
        val windowInsetsController = requireActivity().window.insetsController

        // context — nullable version of requireContext(), safe if you check
        context?.let { ctx ->
            Toast.makeText(ctx, "Hello", Toast.LENGTH_SHORT).show()
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The practical rule in Fragments: use requireContext() for Context needs, requireActivity() only when you specifically need the Activity. The difference matters if you ever move the Fragment to a different host.


The WeakReference anti-pattern

You'll sometimes see this pattern as an attempted fix for Context leaks:

class SomeManager(context: Context) {
    // "I'll use WeakReference so I don't leak"
    private val contextRef = WeakReference(context)

    fun doSomethingWithUI() {
        val ctx = contextRef.get() ?: return
        // use ctx
    }
}
Enter fullscreen mode Exit fullscreen mode

This is better than holding a strong reference, but it's still wrong for different reasons:

  1. If you need the UI context to still be alive, a WeakReference that's been collected gives you null at an unpredictable time — leading to silent no-ops or hard-to-reproduce bugs.
  2. If you don't need the UI context, you should be using applicationContext anyway.

WeakReference<Context> is almost never the right answer. It papers over the real issue: the object's lifetime is incompatible with the context's lifetime. Fix the lifetime problem instead.


The ContextThemeWrapper you didn't know you were using

When you write LayoutInflater.from(activity), you get a LayoutInflater that uses the Activity's theme. But sometimes you need to inflate a view with a different theme than the current Activity — for example, inflating a dark-themed bottom sheet inside a light-themed Activity.

// Inflate with a custom theme, without changing the Activity's theme
val themedContext = ContextThemeWrapper(requireContext(), R.style.ThemeOverlay_App_BottomSheet)
val inflater = LayoutInflater.from(themedContext)
val view = inflater.inflate(R.layout.bottom_sheet_content, container, false)
Enter fullscreen mode Exit fullscreen mode

ContextThemeWrapper wraps any Context and overlays a theme on top. Compose's MaterialTheme uses this mechanism internally when you apply a LocalContentColor or a theme override. Knowing it exists saves you from the wrong solution — changing the Activity theme globally just to style one component.


🧪 The rule of thumb that actually works

Instead of "use applicationContext when in doubt", the rule I use now:

Match the context's lifetime to the consumer's lifetime.

  • Consumer lives as long as the process (singleton, repository, library init) → applicationContext
  • Consumer lives as long as a screen (ViewModel, Adapter, custom View within an Activity) → Activity context, and make sure the consumer doesn't outlive the screen
  • Consumer is inside a FragmentrequireContext(), which gives you the host Activity's context
// Checklist before passing context anywhere:
// 1. How long does the receiver live?
// 2. What operations will it perform with the context?
// 3. Does its lifetime match the context's lifetime?

// If receiver is longer-lived → applicationContext
// If receiver needs UI → Activity context, manage its lifecycle
Enter fullscreen mode Exit fullscreen mode

❓ The interview questions

Question 1 — Mid-level:

"What's the difference between this, applicationContext, and baseContext in an Activity?"

  • this — the Activity itself, which is a Context. Full UI capabilities, lifetime tied to the Activity.
  • applicationContext — the Application object. No UI capabilities, lives as long as the process.
  • baseContext — the wrapped ContextImpl inside the ContextWrapper. Rarely used directly; it's the raw context before any wrapper applies its logic. Calling baseContext from an Activity skips the Activity's own context wrapper logic — almost never what you want.

Question 2 — Senior:

"You're building an SDK that other apps will integrate. Your SDK has a singleton that needs to perform operations like loading resources and starting a background service. How do you handle Context?"

class MySdk private constructor(private val appContext: Context) {

    companion object {
        @Volatile private var instance: MySdk? = null

        fun init(context: Context): MySdk {
            return instance ?: synchronized(this) {
                instance ?: MySdk(
                    // Always store applicationContext — the caller's Activity
                    // will be destroyed; the SDK must outlive it
                    context.applicationContext
                ).also { instance = it }
            }
        }
    }

    fun loadResource(): String {
        // ✅ Resources work fine with applicationContext
        return appContext.getString(R.string.sdk_label)
    }

    fun startBackgroundWork() {
        // ✅ Starting a Service works with applicationContext
        appContext.startService(Intent(appContext, SdkSyncService::class.java))
    }

    fun showDialog(activity: Activity) {
        // ✅ For UI operations, require the caller to pass Activity explicitly
        // Don't store it — just use it for this operation
        AlertDialog.Builder(activity)
            .setMessage("SDK needs permission")
            .show()
    }
}
Enter fullscreen mode Exit fullscreen mode

Key points: always store applicationContext in the singleton, never the caller's Activity. For UI operations, make them instance methods that take an Activity parameter rather than storing it.


Question 3 — Senior:

"LeakCanary reports a Context leak in your app, originating from a custom View. How do you diagnose and fix it?"

First, understand what LeakCanary is telling you: the Context (likely an Activity) is retained after it should have been garbage collected, because something is holding a reference to it.

For a custom View, the most common causes:

// Cause 1: Static reference to a View (which holds its Context)
companion object {
    var lastClickedView: View? = null  // 💀 holds Activity via view.context
}

// Cause 2: Anonymous listener registered on a long-lived object
class MyView(context: Context) : View(context) {
    init {
        EventBus.register(object : EventListener {  // 💀 anonymous class holds outer View
            override fun onEvent(event: Event) { /* ... */ }
        })
        // EventBus holds the listener, listener holds the View, View holds Context
    }
}

// Fix: use weak reference or unregister in onDetachedFromWindow
override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    EventBus.unregister(listener)
}

// Cause 3: Context stored in a non-View object without applicationContext
class ImageCache(val context: Context) {  // 💀 if passed Activity context
    // ...
}
// Fix: ImageCache(context.applicationContext)
Enter fullscreen mode Exit fullscreen mode

Diagnosis approach: read the LeakCanary reference chain bottom-up. The chain shows exactly which object is holding the reference that prevents GC. The fix is usually at the top of that chain — nulling a reference, unregistering a listener, or swapping to applicationContext.


What surprised me revisiting this

  1. applicationContext for LayoutInflater doesn't just look wrong — it silently breaks theme-dependent attributes without throwing an exception. Views inflate but render incorrectly. The kind of bug that shows up in screenshots and is hard to reproduce in isolation.

  2. WeakReference<Context> is a false sense of security. I've written this pattern before and thought I was being careful. Going back through the theory, it just moves the problem rather than fixing it.

  3. ContextThemeWrapper being the mechanism behind Compose's LocalContentColor — I'd used Compose theme overrides for months without ever connecting it to the View system's ContextThemeWrapper. The mental model clicked when I saw them as the same concept.


Tomorrow

Day 7 → Week 1 Recap — six posts in, and I'm already finding more gaps than I expected. A summary of the most important insights from this week, plus the one question I couldn't answer confidently until now.

What's the most surprising Context-related bug you've encountered? Drop it in the comments.


Day 5: Intent, Task & Back Stack — Why launchMode Is a Trap

Top comments (0)