Context, Application & Manifest Explained
ANDROID › System
Every component you write, an Activity, a Service, a custom View, needs a handle into the app's environment: reading a string resource, grabbing a system service, starting another component. That handle is Context, an abstract class.
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
Activity, Service, and Application are all, indirectly, Context subclasses through ContextWrapper. That is the trap interviewers probe for: they are interchangeable by type, so the compiler accepts any of them, but they carry very different lifecycles and scope. Picking the wrong one is one of the most common real bugs in Android codebases, and the rest of this lesson is about exactly when each one is safe to use.
**Application context** is a single instance that lives for the whole process. **Activity context** is scoped to one Activity and is also a themed context, it carries that activity's theme and window token.
The rule of thumb: use Activity context for anything UI-related, inflating themed views, showing a Dialog, starting another Activity. Use Application context for anything long-lived and process-scoped that has no business outliving the screen.
class ImageCache private constructor(context: Context) {
private val appContext = context.applicationContext // safe to hold
}
Getting this backwards in either direction causes a real bug, not just a style nit. The next two chunks show both directions.
Here is the leak in full. If a long-lived object holds a strong reference to an Activity context, that Activity, and its entire view tree, cannot be garbage collected after it is destroyed, even though the system thinks it is gone.
// BAD: static field outlives the Activity
object Holder {
var activity: Activity? = null
}
On rotation, the old Activity is destroyed and a new one is created, but Holder.activity still points at the dead one. That instance, its Views, its Bitmaps, everything reachable from it, stays pinned in memory. Multiply this across a few rotations and you have a serious leak. The fix is either to hold applicationContext instead, or wrap the reference in a WeakReference if you genuinely need the specific Activity.
The mirror-image mistake: using Application context where you needed Activity context. Application context has no theme and no window token, so it cannot inflate a themed layout correctly and it cannot host a Dialog.
// BAD: no window token to attach the dialog to
AlertDialog.Builder(applicationContext)
.setTitle("Error")
.show() // throws BadTokenException
Swap in the Activity, this, inside an Activity, and it works, because that context carries the window token the WindowManager needs to attach the dialog's window to the right screen.
AlertDialog.Builder(this).setTitle("Error").show() // fine
The Application class itself is instantiated once per process, before any Activity or Service starts. Its onCreate() runs on the main thread, which means anything slow you put there directly delays cold start, or worse, risks an ANR before the first frame is even drawn.
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
val cfg = File(filesDir, "config.json").readText() // blocks main thread
}
}
The fix is to defer real work: kick it off on a background dispatcher, or use lazy initialization so it only runs when actually needed, not unconditionally on every cold start. Keep that in mind, because the next chunk explains a subtlety about exactly when your Application's onCreate fires relative to other early hooks in the process.
There is a precise ordering guarantee worth knowing on its own: the system calls every declared ContentProvider's onCreate() after your Application's attachBaseContext(), but before Application.onCreate() runs. That makes a ContentProvider's onCreate() one of the earliest hooks available in the whole process, earlier than any code you write in Application.onCreate() itself.
class MyApp : Application() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base) // runs first
}
override fun onCreate() {
super.onCreate() // runs last, after every ContentProvider.onCreate()
}
}
class MyProvider : ContentProvider() {
override fun onCreate(): Boolean {
// runs after attachBaseContext, before Application.onCreate
return true
}
}
Library authors exploit this: registering a ContentProvider purely to run init code is a trick for getting code to execute before anything in your own Application.onCreate(). The catch is that every ContentProvider is expensive to instantiate, and a library that registers three of them pays that cost three times over at every cold start.
The AndroidX **App Startup** library exists to fix exactly that cost. Instead of every library registering its own ContentProvider, App Startup replaces all of them with a single InitializationProvider, one provider using that same early onCreate() hook, and lets each library declare its ordering explicitly instead of relying on manifest merge order.
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.MyInitializer"
android:value="androidx.startup" />
</provider>
One provider, however many initializers a library needs to register, replaces however many ContentProviders those libraries would otherwise each pay to instantiate.
Each unit of startup work implements the Initializer interface, with two methods. create(context) builds and returns the actual component. dependencies() returns the list of other Initializer classes that must run first.
class AnalyticsInitializer : Initializer<Analytics> {
override fun create(context: Context): Analytics = Analytics.init(context)
// WorkManagerInitializer is guaranteed to run first
override fun dependencies(): List<Class<out Initializer<*>>> =
listOf(WorkManagerInitializer::class.java)
}
App Startup resolves the whole graph of every registered Initializer's dependencies() and creates them in the right order automatically, so you never have to hand-sequence startup code yourself.
Sometimes you want App Startup's automatic discovery for most initializers but need to disable exactly one, maybe a library's default analytics initializer you would rather trigger yourself, later, with different arguments. App Startup discovers initializers via <meta-data> entries a library's manifest merges into yours, so opting one out means removing its entry with a manifest merge rule, tools:node="remove".
<provider android:name="androidx.startup.InitializationProvider" ...>
<meta-data
android:name="com.example.AnalyticsInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
With that entry gone, App Startup skips AnalyticsInitializer entirely during automatic discovery. You then initialize it yourself, wherever and whenever makes sense, by calling AppInitializer.getInstance(context).initializeComponent(AnalyticsInitializer::class.java).
Every Activity has its own lifecycle callbacks, but a lot of real questions are about the whole app: has the user left entirely, backgrounding every screen, or just rotated one Activity through a recreate? Watching a single Activity's onStop() cannot tell you that, since another Activity in the same app might already be in the foreground.
The older tool is registerActivityLifecycleCallbacks() on Application, which fires a callback for every single Activity's lifecycle events individually. It is commonly used for analytics, crash context, and similar per-Activity bookkeeping, but to infer whole-app foreground state from it you would have to track start and stop counts yourself.
ProcessLifecycleOwner does that counting for you. It aggregates every Activity's lifecycle into one process-level Lifecycle, exposing app-wide ON_START/ON_STOP events that fire only when the whole app truly enters or leaves the foreground, not on every individual Activity transition.
ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
analytics.logAppBackgrounded() // fires once, app-wide, not per Activity
}
})
The manifest is the contract between your app and the system. It declares every Activity, Service, BroadcastReceiver, and ContentProvider your app has, the permissions and hardware or software features it needs, and metadata like the App Startup entries from a few chunks back. An undeclared Activity simply cannot be started, the system does not know it exists.
<application>
<activity android:name=".MainActivity" android:exported="true" />
<service android:name=".SyncService" />
</application>
Since API 31, any component with an <intent-filter> must also explicitly set android:exported, or the system refuses to install the app. This closed a security gap where components were unintentionally left reachable by other apps. Permissions and features get their own manifest rules, covered next, and intent filters, the piece that decides which component a given Intent actually reaches, get the chunk after that.
A runtime permission request, ActivityCompat.requestPermissions, only works if the manifest already declares that permission with <uses-permission>. Miss the declaration and the runtime prompt is silently dropped, denied before the user even sees a dialog.
<uses-permission android:name="android.permission.CAMERA" />
Separately, if your app uses hardware but does not strictly require it, mark that with <uses-feature> and android:required="false".
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
By default, <uses-feature> is treated as required, which makes the Play Store filter the app off any device lacking that hardware entirely. Setting required="false" keeps the app installable everywhere, and your code becomes responsible for checking PackageManager.hasSystemFeature() at runtime and disabling the camera-dependent path gracefully on devices that do not have one.
**Intent filters** are how the system matches an implicit Intent to a component, based on action, category, and data. Two combinations show up constantly.
The launcher entry point needs action MAIN and category LAUNCHER.
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
A web deep link needs action VIEW, categories DEFAULT and BROWSABLE, so a browser can trigger it, and a <data> tag matching the URL's scheme and host. Miss BROWSABLE and tapping the link in a browser simply will not offer your app as an option.
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="example.com" />
</intent-filter>
Zoom out and the whole topic is one idea, repeated at increasing scope: the system tracks what it knows exists and for how long, and every bug here comes from mismatching your code's assumptions to that. A Context mismatch leaks a screen. A blocking Application.onCreate delays every cold start. An undeclared manifest entry is a component the system has never heard of. An intent filter is how you tell the system when something you built is allowed to be reached from outside your own code, by the launcher, by a browser link, by another app.
If an interviewer asks you to explain context leaks, do not just recite applicationContext versus Activity context. Say why: a strong reference outliving its owner keeps the garbage collector from doing its job, and that same reasoning is what should make you pause before storing an Activity, a View, or a Fragment anywhere that outlives them, a singleton, a static field, a long-lived listener, a coroutine scope that isn't tied to the screen. That is the answer that shows you reason about lifecycle, not just API names.