Activity, Service, BroadcastReceiver and ContentProvider. Each is declared in the manifest and is an entry point the system itself can start into your app; Application is not an entry point, only the process-wide object created before any of them.
Activity: one screen the user interacts with, started by an Intent.
Service: work without a UI: a foreground service the user can see (music, navigation) or a bound service other code calls into. Deferrable background work goes to WorkManager instead.
BroadcastReceiver: reacts to system or app events such as BOOT_COMPLETED and must finish quickly.
ContentProvider: exposes data to other apps or processes through content:// URIs; FileProvider is one.
AndroidJunior
Android Fundamentals
Activity, Fragment, lifecycle, Intent, Manifest, configuration changes
Questions closed
0 / 25
- 0Review
- 16Asked often
Questions
onCreate → onStart → onResume bring an Activity up, onPause → onStop → onDestroy take it down. onCreate runs once per instance, so one-time setup such as setContentView() belongs there. A routing Activity that calls finish() in onCreate goes straight to onDestroy.
**onCreate**: set the content (setContentView, or setContent {} in Compose), create bindings, read savedInstanceState. In onResume the view tree would be rebuilt every time the user came back.
**onStart / onStop**: visible / no longer visible. Start and stop UI subscriptions here, or use repeatOnLifecycle(STARTED).
**onResume / onPause**: focused / losing focus. Keep onPause fast: the next Activity's onResume waits for it.
**onSaveInstanceState**: called after onStop on API 28+, to save small UI state into a Bundle.
**onDestroy**: isFinishing tells a real finish from a configuration change, which also destroys the instance.
**onDestroy alone**: calling finish() inside onCreate (a routing or trampoline Activity) goes straight to onDestroy. And when the system kills a background process, no callback runs at all, so never rely on onDestroy to save data.
Rotation is a configuration change: the Activity is destroyed and recreated with the new resources, so its fields are lost. A ViewModel survives the recreation, and the saved-state Bundle (onSaveInstanceState, SavedStateHandle, rememberSaveable) also survives process death.
Other configuration changes do the same: language, dark mode, font scale, a keyboard attached, a multi-window resize.
**ViewModel**: kept in the ViewModelStore across recreation; holds loaded data and screen state. It is gone when the process dies.
Saved-state Bundle: small UI state: a query, the selected tab, an id. SavedStateHandle hands it to the ViewModel, rememberSaveable to a composable.
Why a Bundle and not SharedPreferences: the Bundle belongs to this screen in this task and is dropped when the user closes it. SharedPreferences would keep the value forever, leak it into the next launch and cost a disk write.
**android:configChanges**: opts out of recreation, and you then handle new resources yourself in onConfigurationChanged. Fine for a video player; not a generic fix for lost state.
A Context is your handle on the system: resources, preferences, starting components. The kinds differ mainly in lifetime, and an Activity context must never be held by anything that outlives the Activity.
Application context: applicationContext, alive for the whole process. Singletons, databases, DataStore and repositories should keep this one.
Activity context: one per Activity instance, dead once it is destroyed. It carries the Activity's theme and window, so dialogs, layout inflation and anything themed need it; a dialog shown from the application context throws BadTokenException.
Service and receiver contexts: short-lived; a receiver's context is valid only during onReceive.
The classic leak: a static field, a singleton or an unregistered listener holding an Activity keeps the Activity, its whole view tree and its bitmaps in memory. Narrow to applicationContext at the constructor of anything long-lived; with Hilt inject @ApplicationContext. A ViewModel should hold no Context at all; keep a string resource id in the state and resolve it in the UI.
In Application.onCreate(): it runs once per process, on the main thread, before any Activity, Service or receiver. It must stay small, because it sits on every cold start, and it must not hold app state, because a killed process takes every field with it.
The process starts for many reasons besides a launcher tap: a broadcast, a WorkManager job, a ContentProvider query. Each runs Application.onCreate with no UI at all.
Manifest-declared ContentProviders are created after attachBaseContext and before Application.onCreate; that is how libraries initialise themselves invisibly. androidx.startup merges them into one provider with explicit dependencies.
Belongs here: the DI graph (@HiltAndroidApp), crash reporting, logging.
Does not: disk or network reads, eager SDK setup, the current user or cart in a field. After process death the user returns to the same screen while every Application field and object singleton is back at its initial value; durable state goes to Room, DataStore or SavedStateHandle.
For app-wide foreground/background tracking, observe ProcessLifecycleOwner instead of counting Activities.
The manifest tells the system what the app contains before any of its code runs: its components, the intents they accept, the permissions and hardware it needs. At build time Gradle merges your manifest with those of every library and build variant into one file.
Components: <activity>, <service>, <receiver>, <provider>. Any component with an <intent-filter> must set android:exported explicitly (Android 12+).
Permissions and features: <uses-permission>, <uses-feature> (Play also filters devices by it), <queries> for package visibility.
Application attributes: theme, icon, the Application class, backup and network security config.
Not here: applicationId, minSdk, targetSdk and the version come from build.gradle.kts and are injected.
Merging: priority goes build variant, then main, then libraries. Conflicts are resolved with tools:replace, and an unwanted element from a library is removed with tools:node="remove". The merged file (build/intermediates/merged_manifests/) and the report (build/outputs/logs/manifest-merger-*-report.txt) show where a surprise permission came from; Android Studio's Merged Manifest tab shows the same.
compileSdk decides which APIs you can compile against, minSdk the oldest Android version that can install the app, and targetSdk which platform behaviour changes the system applies to you. Raising targetSdk opts you into every behaviour change up to that level at once.
compileSdk affects the build only; raising it never changes runtime behaviour, it just lets you reference newer APIs.
minSdk is a product decision based on your users. Calling a newer API without a Build.VERSION.SDK_INT check or @RequiresApi is the NewApi lint error.
targetSdk is the level you have tested against. The system keeps compatibility shims for apps targeting older levels; raise it and you get the new rules: the notification permission (33), foreground-service types (34), edge-to-edge by default (35), and so on.
Play requires a recent targetSdk for updates, so the bump cannot be postponed forever. Read the behaviour-change list for every level you skip, test permissions, background work and file access, and ship the bump on its own.
All three live in build.gradle.kts, not in the manifest.
Android compares the qualifiers in each folder name with the current device configuration, discards every folder that contradicts it, and among the rest picks the one that wins in a fixed precedence order. The unqualified folder is the fallback.
Qualifiers live in folder names: values-ru, values-night, drawable-xxhdpi, layout-sw600dp, values-v33.
Precedence is fixed: MCC/MNC, then language and region, layout direction, smallest width, available width and height, screen size, orientation, night mode, density, and platform version last.
The default folder (values/) must contain every resource, or a device matching no qualifier crashes with Resources.NotFoundException. Lint flags a missing default.
Density is the one qualifier that is approximated: with no exact bucket the system takes the nearest and scales. A VectorDrawable avoids buckets altogether.
A configuration change re-runs the lookup, which is why changing the language or dark mode recreates the Activity. Branch for large screens with sw600dp or, in Compose, with window size classes in code.
An Intent is a message asking the system to start a component. An explicit intent names the exact class; an implicit one names an action and data, and the system finds an app whose <intent-filter> matches.
Explicit: Intent(context, DetailActivity::class.java); used inside your own app.
Implicit: ACTION_VIEW, ACTION_SEND, ACTION_DIAL. Resolution matches the action, the categories (an Activity needs CATEGORY_DEFAULT) and the data scheme and MIME type. Several matches show a chooser; none throws ActivityNotFoundException, so catch it. Since Android 11, querying other apps also needs a <queries> entry.
Extras: data travels in a Bundle, so keep it small: an id, not an object list.
Results: the Activity Result API: registerForActivityResult(contract) { }, then launch(). It replaced the deprecated startActivityForResult/onActivityResult. Register before the screen is STARTED (as a field), or you get an IllegalStateException.
**PendingIntent**: an intent wrapped with your app's identity so another app (notifications, alarms) can fire it later.
A Fragment is a reusable piece of UI with its own lifecycle that lives inside an Activity. Its state can never be ahead of the host's, and its view can be destroyed while the Fragment object lives on, so the view has its own lifecycle: viewLifecycleOwner.
Callbacks: onAttach → onCreate → onCreateView → onViewCreated → onStart → onResume, then onPause → onStop → onDestroyView → onDestroy → onDetach.
Two lifecycles: when a Fragment is replaced and put on the back stack, only onDestroyView runs; the instance stays and gets a new view on Back. Observe UI with viewLifecycleOwner, or a listener keeps updating a dead view and leaks it.
ViewBinding: set the binding to null in onDestroyView for the same reason.
When to use: View-based apps with one Activity and many screens, tablet master-detail layouts, reusable UI pieces. A Compose-first app usually has no Fragments: one Activity hosts composable destinations.
The FragmentManager recreates fragments itself after a configuration change or process death, and it can only call the empty constructor. Data must travel in the arguments Bundle, which the system saves and restores for you.
On recreation FragmentFactory.instantiate calls the no-argument constructor and reattaches the saved arguments. With only a one-argument constructor it throws Fragment.InstantiationException; a field set from a setter would silently come back empty.
Set arguments before the Fragment is added, which is why the newInstance(...) factory with bundleOf(...) is the idiom.
Keep the Bundle to identity (an id, a small enum), not loaded objects; it becomes part of the saved state.
SavedStateHandle in the Fragment's ViewModel is pre-filled with the same arguments, so the ViewModel can read the id directly. With Navigation, Safe Args or typed routes build this Bundle for you.
Dependencies (not screen data) can go through the constructor only with a custom FragmentFactory assigned to the FragmentManager before super.onCreate. Hilt does not do this: an @AndroidEntryPoint Fragment gets its dependencies by field injection.
Each permission has a protection level. Normal ones are granted at install from the manifest alone, signature ones only to apps signed with the same certificate as the declaring app, and dangerous ones must also be requested at runtime and can be revoked by the user at any time.
Normal: INTERNET, VIBRATE: low risk, the manifest entry is enough.
Signature: for components of apps from one developer; no dialog.
Dangerous: camera, location, contacts, POST_NOTIFICATIONS (Android 13+). Declare in the manifest, check with ContextCompat.checkSelfPermission every time, request with ActivityResultContracts.RequestPermission.
Special: overlays, exact alarms, all-files access: no dialog, you send the user to a Settings screen.
"Don't ask again": since Android 11, two denials make the system stop showing the dialog and return denied immediately. shouldShowRequestPermissionRationale tells you when to explain first; after a permanent denial the only path is the app's Settings page.
Prefer APIs that need no permission at all, such as the Photo Picker.
onPause means the Activity has lost focus but may still be visible; onStop comes only once it is fully hidden. A translucent or dialog-themed Activity on top, picture-in-picture and, before Android 10, losing focus in split screen all give onPause alone.
**An AlertDialog or DialogFragment: not a separate Activity, just another window of yours: no lifecycle callback fires at all. A classic trick question.
A dialog-themed or translucent Activity on top**: onPause only; you are still visible underneath.
Picture-in-picture: entering PiP pauses the Activity while it stays on screen; onPictureInPictureModeChanged reports the switch. Keep video playing until onStop, not onPause.
Multi-window (Android 10+): every visible Activity is RESUMED at the same time. Only one is top-resumed, reported by onTopResumedActivityChanged, and exclusive resources such as the camera follow that callback, not onResume.
Resizing in split screen: a configuration change, so the Activity is recreated unless it handles the change itself.
Which callback means foreground: on Android 10+ onTopResumedActivityChanged(true); onResume only means the Activity is visible and interactive, and several Activities can be resumed at once.
Extras and saved state are flattened into a Parcel and sent through Binder; Parcelable is the explicit, fast format, Serializable is reflection-based and slower. Binder's buffer is about 1 MB per process, so a big Bundle throws TransactionTooLargeException.
A Bundle holds primitives, String, arrays and Parcelable or Serializable objects; anything else fails at runtime.
Write Parcelable with the kotlin-parcelize plugin: @Parcelize generates writeToParcel and CREATOR. Hand-written parcels break when the read order drifts from the write order.
Serializable works through reflection, creates many temporary objects and is measurably slower. It remains for Java interop.
Since API 33 the untyped getParcelableExtra(name) is deprecated; use IntentCompat.getParcelableExtra(intent, key, Order::class.java) or BundleCompat.
The 1 MB buffer is shared by all transactions in flight. A list of hundreds of rows or a Bitmap in onSaveInstanceState throws TransactionTooLargeException, often after onStop, far from the code that put it there.
The rule that avoids both problems: pass an id and let the next screen load the object from its repository.
When your app is in the background, the system may kill its process to free memory. When the user returns, Android restores the task and the top Activity from saved state, so only saved-state Bundles and data on disk survive; ViewModels, singletons and static fields start empty.
Survives: the back stack, Intent extras, Fragment arguments, onSaveInstanceState, SavedStateHandle, rememberSaveable, the Navigation back stack, Room, DataStore and files.
Lost: ViewModel instances, object singletons, Application fields, in-memory caches, running coroutines.
Versus rotation: only process death loses the ViewModel. Swiping the app away from Recents or Force stop is different again: the task and its saved state are discarded and nothing is restored.
Typical symptoms: an UninitializedPropertyAccessException on return, an empty screen, "the app restarted itself".
Reproduce: send the app to the background and run adb shell am kill <package>, which only kills a background process, then reopen it from Recents. The "Don't keep activities" developer option destroys Activities but keeps the process, so it does not catch lost singletons.
A one-shot result goes through the Fragment Result API (setFragmentResult / setFragmentResultListener); shared, ongoing state goes in a ViewModel scoped to their common owner. Holding a reference to another Fragment or casting the Activity to a listener interface is the legacy approach.
Fragment Result API: the FragmentManager keeps the result until the listener is at least STARTED and survives recreation. Siblings use parentFragmentManager; a parent listening to its child uses childFragmentManager.
Shared ViewModel: by activityViewModels() or by navGraphViewModels(R.id.checkout); both Fragments observe one StateFlow. Right for master-detail and multi-step forms.
Interface to the host: context as Listener in onAttach. It works but couples the Fragment to one Activity and is easy to leak.
Never keep a reference to another Fragment: after recreation it points to a dead instance.
With Navigation: results go through previousBackStackEntry?.savedStateHandle, which also survives process death.
replace() removes the fragments currently in the container and adds the new one; add() puts the new one on top and leaves the old one alive with its view. addToBackStack() records the transaction so Back reverses it, and childFragmentManager manages fragments nested inside a fragment.
**add**: the old fragment stays RESUMED with its view: screens overlap, taps fall through a transparent background, memory grows. Useful for overlays.
**replace: the old fragment's view is destroyed; the instance survives only on the back stack.
addToBackStack(name)**: without it Back finishes the Activity from a detail screen. popBackStack(name, POP_BACK_STACK_INCLUSIVE) returns to a named point.
Which manager: supportFragmentManager belongs to the Activity, parentFragmentManager is the one holding this fragment, childFragmentManager is for fragments inside it (tabs, a ViewPager2). The wrong one gives children that outlive their parent.
**setReorderingAllowed(true)**: let the manager optimise the transaction; required for correct transitions.
commit() schedules the transaction on the main thread, commitNow() runs it synchronously, and commitAllowingStateLoss() skips the saved-state check. The crash means you committed after the Activity saved its state, so the transaction would be lost if the process died.
onSaveInstanceState runs when the Activity stops. A transaction committed afterwards is not in the saved state, so the FragmentManager throws IllegalStateException rather than silently losing a screen.
Usual cause: a callback landing while the app is in the background: a network response, an observer not tied to a lifecycle, a dialog shown on completion.
**commitAllowingStateLoss()**: turns the crash into a screen that may never appear. The real fix is to deliver the event only while the UI is STARTED, with repeatOnLifecycle.
**commitNow()**: executes immediately, but cannot be combined with addToBackStack(). executePendingTransactions() flushes everything scheduled with commit().
In a Navigation-based app the library makes these transactions for you, but the lifecycle rule is the same.
A launch mode decides whether starting an Activity creates a new instance or reuses an existing one. To go from A-B-C-D back to the existing A, start A with FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP: B, C and D are destroyed and A receives onNewIntent.
**standard: a new instance every time; the default and almost always right.
singleTop**: if the Activity is already on top, reuse it and call onNewIntent instead of stacking a copy. Typical for a screen opened from notifications or deep links.
**singleTask: one instance per task; starting it clears everything above it. Overused, because it silently destroys the screens on top.
singleInstance: the Activity is alone in its own task. Rare.
Flags per launch**: CLEAR_TOP without SINGLE_TOP destroys and recreates a standard A (a singleTop or singleTask A gets onNewIntent). For logout use FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK: the old task is wiped and a fresh A starts.
**taskAffinity**: which task an Activity prefers to belong to; it matters together with singleTask and NEW_TASK.
A single-Activity Compose app rarely touches this: the NavController owns the back stack, and only the main Activity's mode matters for deep links.
A BroadcastReceiver reacts to system-wide or app events; a manifest receiver can start your process, a runtime one exists only while registered. Android 8 stopped delivering most implicit broadcasts to manifest receivers, and Android 14 requires an exported flag in registerReceiver.
Manifest receiver: works even when the app is not running, but since API 26 only for explicit broadcasts and a short exempt list (BOOT_COMPLETED, LOCKED_BOOT_COMPLETED, MY_PACKAGE_REPLACED and a few more).
Runtime receiver: registerReceiver in code, unregistered in the mirrored lifecycle callback, or it leaks the Context.
Android 14: apps targeting 34 must pass RECEIVER_EXPORTED or RECEIVER_NOT_EXPORTED for any non-system broadcast, otherwise SecurityException. Use ContextCompat.registerReceiver for older versions.
**onReceive**: runs on the main thread, and the process is guaranteed to live only during the call (about 10 s before an ANR). For longer work call goAsync() or enqueue WorkManager.
Replacements: CONNECTIVITY_ACTION gave way to ConnectivityManager.registerNetworkCallback; LocalBroadcastManager is deprecated in favour of a SharedFlow.
A ContentProvider exposes data through content:// URIs with a query/insert/update/delete interface, and the system enforces permissions on it. You need one to share data with other apps or processes; for your own screens a repository over Room is enough.
Access: clients call contentResolver.query(uri, ...); the provider's methods run on Binder threads, so they must be thread-safe.
Startup: providers are created before Application.onCreate, which is why libraries use them to initialise themselves.
Typical uses: contacts, media and calendar are providers; your own app writes one for widgets, search suggestions or a sister app.
**FileProvider**: a ready-made provider for sharing files. Since Android 7 a file:// URI in an intent throws FileUriExposedException. Declare the provider with exported="false", grantUriPermissions="true" and an XML list of shareable folders, then turn a file into a URI with FileProvider.getUriForFile.
The grant: the receiving app gets access only through FLAG_GRANT_READ_URI_PERMISSION on the same intent; without it, it fails with SecurityException. The grant is temporary.
Derive the authority from applicationId: a hard-coded copy-pasted one conflicts with another app and blocks installation.
It touches a view from a background thread, which throws CalledFromWrongThreadException: only the thread that created the view hierarchy, the main thread, may change it. The main thread runs a Looper that processes a MessageQueue, and a Handler posts work into that queue from any thread.
How the main thread works
**MessageQueue**: messages and Runnables ordered by time.
**Looper**: Looper.loop() takes the next message and dispatches it, forever. One per thread, stored in a ThreadLocal, which is how Looper.myLooper() finds it. The main one is Looper.getMainLooper(); HandlerThread is a background thread with its own Looper. The endless loop is not an ANR: with an empty queue the thread sleeps in nativePollOnce, and an ANR means one message (such as input, 5 s) was not handled in time.
**Handler**: bound to a Looper; post() and sendMessage() enqueue work on that thread. view.post {}, runOnUiThread {} and Dispatchers.Main all use it underneath.
The fix today
lifecycleScope.launch with withContext(Dispatchers.IO) for the call: the result comes back on the main thread and the work is cancelled with the screen. The raw Thread also leaks the Activity and is never cancelled.
The crash is intermittent: the check lives in ViewRootImpl, so a write before the view is attached passes. Do not trust "it worked once".
The delayed Runnable is never cancelled. The main Looper runs it even after the Activity is stopped or finished, so leaving the splash still opens MainActivity two seconds later, and after finish or a rotation the queued message keeps the destroyed Activity in memory until it fires.
Why: postDelayed puts a Message into the main MessageQueue, and the queue knows nothing about lifecycles. The lambda captures this, so the chain queue → message → lambda → Activity holds the instance. On Android 12+ Back on a launcher Activity only moves the task to the background: the Activity is stopped, not destroyed, so no destroy callback runs. The dead-instance leak applies to finish and rotation, where the old and new instances both fire and two MainActivity screens land on the back stack.
The fix: schedule in onStart and removeCallbacks in the mirrored onStop. That covers Back, rotation and Home: the timer restarts when the user returns instead of firing in the background.
Coroutine alternative: repeatOnLifecycle(STARTED) { delay(2_000); … } gives the same start/stop pairing. A plain lifecycleScope.launch is cancelled only when the Activity is destroyed, which Back does not trigger on a root launcher Activity.
Catch it: lint does not flag it. LeakCanary reports the retained Activity for long delays; a Robolectric test can finish the Activity, idle the main Looper past the delay and assert no Activity was started.
Since Android 12 installSplashScreen() from core-splashscreen replaces a custom splash Activity.
The Kotlin compiler produces JVM bytecode, D8 (or R8 in release builds) converts it into DEX bytecode, and the Android Runtime (ART) executes it. Since Android 7, ART combines an interpreter, a JIT compiler and profile-guided AOT compilation in the background.
DEX: a compact, register-based format with all classes of an app in classes.dex files. One file holds up to 65,536 method references; beyond that the build produces several (multidex), handled natively since Android 5, so no setup is needed with minSdk 21+.
Dalvik (up to Android 4.4): interpreter plus JIT, so code was recompiled on every launch.
ART, Android 5–6: full AOT compilation at install with dex2oat: fast to run, slow to install, large on disk.
ART, Android 7+: hybrid: new code is interpreted and JIT-compiled, hot methods are recorded into a profile, and dex2oat compiles them ahead of time while the device is idle and charging.
Why it matters: the first launches after install are slower until the profile is compiled. Cloud profiles from Play and Baseline Profiles shipped in the app close that gap. ART is updatable through Play system updates since Android 12, so runtime fixes no longer wait for an OS release.
Yes: android:process on a component runs it in a separate process with its own memory, its own Application instance and its own singletons. Processes talk through Binder, the kernel IPC mechanism behind every Intent, system-service call and bound service.
Several processes in one app
android:process=":remote" makes a private process. Application.onCreate runs again in each one, so guard initialisation by process name (Application.getProcessName(), API 28+).
Nothing in memory is shared: singletons, caches and Room's invalidation (it needs enableMultiInstanceInvalidation()) differ per process. SharedPreferences is not process-safe; DataStore has MultiProcessDataStoreFactory.
Reasons: isolating a crash-prone SDK or a media player from the UI, or getting a separate memory budget. Each process costs startup time and RAM.
What Binder gives you
One data copy between processes, the caller's identity (Binder.getCallingUid()) for permission checks, and death notifications through linkToDeath.
A call blocks the caller until it returns; the callee runs it on a Binder thread pool (up to about 16 threads), not on its main thread, so the implementation must be thread-safe. oneway makes a call asynchronous.
The transaction buffer is about 1 MB per process, which is where TransactionTooLargeException comes from.
Ways to use it, simplest first
**Messenger**: a Handler-based queue; calls are processed one at a time.
AIDL: you describe an interface, the build generates Stub and Proxy; for typed, concurrent calls.
Bound service: bindService(); the service returns an IBinder from onBind, and the client gets it in onServiceConnected. onServiceDisconnected fires when the remote process dies.
**ContentProvider**: when what you share is data with query semantics.