Day 6/100: Context in Android β The Wrong One Will Leak Your Entire Activity
<p><em>This is Day 6 of my <a href="https://dev.to/hoangshawn/series/37575">100 Days to Senior Android Engineer</a> series. Each post: what I thought I knew β what I actually learned β interview implications.</em></p> <h2> π The concept </h2> <p><code>Context</code> 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.</p> <p>But <code>Context</code> 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.</p> <p>Not a theoretical one. A r
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:
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:
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:
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.
// β 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
// β Show Dialog with Activity context AlertDialog.Builder(this) .setMessage("Are you sure?") .show()`
Enter fullscreen mode
Exit fullscreen mode
// β 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:
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:
-
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.
-
If you don't need the UI context, you should be using applicationContext anyway.
WeakReference 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.
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 Fragment β requireContext(), which gives you the host Activity's context
// 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 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
-
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.
-
WeakReference 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.
-
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
DEV Community
https://dev.to/hoangshawn/day-6100-context-in-android-the-wrong-one-will-leak-your-entire-activity-3o3kSign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
Knowledge Map
Connected Articles β Knowledge Graph
This article is connected to other articles through shared AI topics and tags.


Discussion
Sign in to join the discussion
No comments yet β be the first to share your thoughts!