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
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
}
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
}
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)
// ❌ 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()
// ❌ 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
}
)
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()
}
}
}
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
}
}
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
WeakReferencethat's been collected gives younullat 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
applicationContextanyway.
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)
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
// 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
❓ The interview questions
Question 1 — Mid-level:
"What's the difference between
this,applicationContext, andbaseContextin an Activity?"
-
this— the Activity itself, which is aContext. Full UI capabilities, lifetime tied to the Activity. -
applicationContext— theApplicationobject. No UI capabilities, lives as long as the process. -
baseContext— the wrappedContextImplinside theContextWrapper. Rarely used directly; it's the raw context before any wrapper applies its logic. CallingbaseContextfrom 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()
}
}
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)
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
applicationContextforLayoutInflaterdoesn'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<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.ContextThemeWrapperbeing the mechanism behind Compose'sLocalContentColor— I'd used Compose theme overrides for months without ever connecting it to the View system'sContextThemeWrapper. 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)