The building blocks every Android engineer is expected to know cold — Activity & Fragment lifecycles, Intents, Context, Services, Broadcast Receivers, and Content Providers explained with the depth interviewers probe for.
♻️ Activity Lifecycle
🧩 Fragments
📨 Intents
🌍 Context
⚙️ Services
Activity lifecycle
The Activity lifecycle is the most-asked Android interview topic, full stop. Android can destroy and recreate your Activity at any time — on rotation, when the user navigates away, when the system is low on memory. The lifecycle callbacks are the OS’s way of telling you where you are so you can manage resources correctly. Get this wrong and you leak memory, crash on rotation, or drain the battery.
There are seven core callbacks. Think of them in three concentric circles: the entire lifetime (onCreate → onDestroy), the visible lifetime (onStart → onStop), and the foreground lifetime (onResume → onPause).
📋 Activity lifecycle — state machine with all seven callbacks
What to do in each callback
Callback
What happens
Do here
Don’t do here
onCreate
Activity created (or recreated after kill)
Inflate layout, set binding, init ViewModel, setup RecyclerView
Heavy I/O, network calls directly
onStart
Activity visible but not interactive
Register UI-bound listeners, start animations that need visibility
Pause camera, pause playback, release sensors, commit unsaved data
Slow saves, heavy operations (must be fast)
onStop
Activity fully hidden
Release heavy resources, cancel non-critical network requests, save state
Nothing that must survive process death
onDestroy
Activity being torn down permanently or recreated
Final cleanup, cancel coroutines if needed (though ViewModel scope handles most)
UI operations (view is gone)
Configuration changes & saved state
By default, Android destroys and recreates the Activity on rotation. Your UI state is lost unless you protect it. There are two mechanisms:
ViewModel survives configuration changes because it’s stored in a ViewModelStore that outlives individual Activity instances. This is the right place for all UI-related state that needs to survive rotation — lists, selected items, search queries. The ViewModel is destroyed only when the user navigates away or calls finish().
onSaveInstanceState / onRestoreInstanceState survive both configuration changes and process death. Use this for transient UI state that the ViewModel shouldn’t hold — scroll position, expanded state of an accordion, text field content. The bundle size is limited (~1MB) and it’s serialisation-based, so only small primitives and Parcelables belong here.
Activity lifecycle — key patterns
classProductActivity : AppCompatActivity() {
private val vm: ProductViewModelbyviewModels()
private lateinit var binding: ActivityProductBindingoverride funonCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProductBinding.inflate(layoutInflater)
setContentView(binding.root)
// ViewModel survives rotation — observe, don’t re-fetch
vm.products.observe(this) { adapter.submitList(it) }
// Restore transient UI state
savedInstanceState?.getString("query")?.let { binding.searchBar.setText(it) }
}
override funonResume() {
super.onResume()
// Start things that need the foreground: camera, sensors, location
}
override funonPause() {
super.onPause()
// MUST be fast — next activity’s onResume blocks until this returns// Release camera, pause media player
}
override funonStop() {
super.onStop()
// Activity fully hidden — release heavy resources
}
override funonSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// Save small transient UI state (scroll position, input text)
outState.putString("query", binding.searchBar.text.toString())
}
}
⚠️ The onPause performance trap
The system will not show the next Activity until your onPause() returns. If you do anything slow here — disk writes, network calls, long loops — there’s a visible delay before the next screen appears. Keep onPause() fast. Move saves and cleanup to onStop() where there’s no user-visible impact.
Fragment lifecycle
A Fragment has its own lifecycle, but it’s nested inside the host Activity’s lifecycle and must always be considered relative to it. When the Activity is paused, all its Fragments are paused too. Fragments add several extra callbacks to manage their views separately from their own existence — the most important being onCreateView / onDestroyView, which let a Fragment’s view be destroyed and recreated without destroying the Fragment itself (common in the back stack).
📋 Fragment lifecycle — the extra view layer and host Activity relationship
⚠️ The viewBinding memory leak
When using ViewBinding in Fragments, you must null out the binding reference in onDestroyView(). A Fragment can outlive its view — it stays in the back stack while its view is destroyed. If you hold a non-null binding, you’re holding a reference to the destroyed view hierarchy, leaking the entire view tree.
Fragment — correct lifecycle usage with ViewBinding
classProductFragment : Fragment(R.layout.fragment_product) {
privatevar _binding: FragmentProductBinding? = nullprivate val binding get() = _binding!! // safe to use between onCreateView and onDestroyViewprivate val vm: ProductViewModelbyviewModels()
override funonCreateView(inflater: LayoutInflater, container: ViewGroup?, state: Bundle?): View {
_binding = FragmentProductBinding.inflate(inflater, container, false)
return binding.root
}
override funonViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ⭐ Best place for ALL view setup: RecyclerView, click listeners, observers// Use viewLifecycleOwner (not this!) for LiveData observation
vm.products.observe(viewLifecycleOwner) { adapter.submitList(it) }
binding.btnLoad.setOnClickListener { vm.loadProducts() }
}
override funonDestroyView() {
super.onDestroyView()
_binding = null// prevent memory leak — MANDATORY
}
}
Fragment vs Activity — when to use each
Activity
Fragment
Represents
A full screen / app entry point
A reusable portion of a screen
Navigation
Intent-based, separate back stack per task
FragmentManager back stack, Navigation component
Sharing data
Via Intent extras or SharedViewModel (complex)
Shared ViewModel trivial across Fragments in same Activity
Reuse
Low — heavyweight, owns a window
High — same Fragment in different screens / tablets
Modern guidance
Single-Activity app preferred
Navigation Component + Fragments per screen
Intents
An Intent is a messaging object that requests an action from another app component. It’s the glue between components — Activities, Services, and Broadcast Receivers all communicate via Intents. The key distinction interviewers probe: explicit (you specify the exact component) vs implicit (you describe what you want done and let the system find a matching component).
// Explicit Intent — within your app, you know the targetval intent = Intent(this, DetailActivity::class.java).apply {
putExtra("product_id", product.id)
putExtra("product", product) // must be Parcelable or Serializable
}
startActivity(intent)
// Implicit Intent — request an action, let system find the handlerval shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"putExtra(Intent.EXTRA_TEXT, "Check out this product!")
}
// Always check a handler exists before firing implicit intentsif (shareIntent.resolveActivity(packageManager) != null) {
startActivity(Intent.createChooser(shareIntent, "Share via"))
}
// Common implicit intents// Open URLIntent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
// Dial phoneIntent(Intent.ACTION_DIAL, Uri.parse("tel:+91-9999999999"))
// Send emailIntent(Intent.ACTION_SENDTO, Uri.parse("mailto:support@example.com"))
// ActivityResultLauncher — modern replacement for startActivityForResultprivate val pickImageLauncher = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri ->
uri?.let { binding.ivProduct.setImageURI(it) }
}
// Launch it
pickImageLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
// PendingIntent — a token that lets another app/system fire an Intent on your behalf// Used in: Notifications, AlarmManager, Widgets, Shortcutsval pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT orPendingIntent.FLAG_IMMUTABLE
)
Context
Context is Android’s god object — it provides access to resources, the filesystem, system services, and the ability to launch Activities, send Broadcasts, and bind Services. Every operation that touches Android’s OS layer needs a Context. The distinction between Application context and Activity context is one of the most common interview questions, and getting it wrong in production causes memory leaks.
📋 Context hierarchy — what each type can and cannot do
Context — when to use which, and avoiding leaks
// Application context — lives as long as the app process// Safe to store in singletons, repositories, ViewModelsclassImageCache(private val context: Context) {
// ✔ Pass applicationContext when creating singletonsprivate val diskCache = DiskCache(context.applicationContext)
}
// Activity context — tied to that Activity’s lifecycle// Use for: inflating views, showing dialogs, accessing themed resourcesval dialog = AlertDialog.Builder(this) // ✔ Activity context — dialog needs a windowval inflater = LayoutInflater.from(this) // ✔ Activity context — correct theme// ⚠ Common leak: storing Activity context in a singletonobjectBadSingleton {
var context: Context? = null// ✗ If you store an Activity here, it never GC’d
}
// Fix: store applicationContext insteadobjectGoodSingleton {
lateinit var appContext: Contextfuninit(context: Context) { appContext = context.applicationContext }
}
// getSystemService — get OS-level services via Contextval connectivityManager = context.getSystemService(ConnectivityManager::class.java)
val notificationManager = context.getSystemService(NotificationManager::class.java)
val inputMethodManager = context.getSystemService(InputMethodManager::class.java)
Use case
Context to use
Why
Inflate a layout / View
Activity
Needs the Activity’s theme for correct styling
Show an AlertDialog
Activity
Dialog must attach to an Activity’s window
Room database / Retrofit singleton
Application
Lives forever; Activity context would leak
WorkManager / AlarmManager
Application
May fire when Activity is gone
getResources / getString
Either
Activity gives theme-aware resources
startActivity from ViewModel
Application + FLAG_NEW_TASK
ViewModel shouldn’t hold Activity; needs new task flag
Services
A Service is an app component that performs long-running operations in the background without a UI. It runs on the main thread by default — this catches many developers off guard. If you do network or disk work in a Service, you still need to offload it to a coroutine or thread. There are three flavours of service that map to three distinct use cases.
📋 Services — started, bound, and foreground
Services — started, bound, and foreground patterns
// ── Started Service ────────────────────────────────────────────────────────classSyncService : Service() {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override funonStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
scope.launch { performSync() } // offload to coroutine — service is on main thread!returnSTART_NOT_STICKY// don’t restart after kill
}
override funonBind(intent: Intent?) = null// not a bound serviceoverride funonDestroy() { scope.cancel() }
}
// ── Bound Service ─────────────────────────────────────────────────────────classMusicService : Service() {
inner classMusicBinder : Binder() {
fungetService() = this@MusicService
}
private val binder = MusicBinder()
override funonBind(intent: Intent?): IBinder = binder
funplay(track: String) { /* start playback */ }
}
// In Activity/Fragment:private var musicService: MusicService? = nullprivate val conn = object : ServiceConnection {
override funonServiceConnected(name: ComponentName, binder: IBinder) {
musicService = (binder asMusicService.MusicBinder).getService()
}
override funonServiceDisconnected(name: ComponentName) { musicService = null }
}
// ── Foreground Service ────────────────────────────────────────────────────classDownloadService : Service() {
override funonStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildNotification()
startForeground(1, notification) // must call within 5s on API 26+returnSTART_STICKY
}
override funonBind(intent: Intent?) = null
}
Broadcast Receivers
A Broadcast Receiver listens for system-wide or app-level events delivered as Intents. Think of it as a publish-subscribe bus at the Android OS level — the battery going low, the network coming back online, the screen turning off, or your own app sending a custom event. The receiver runs on the main thread and has a very tight time budget: you have about 10 seconds before the system ANRs. Anything heavier than a quick state change must be handed off immediately to a coroutine, Service, or WorkManager.
Static vs dynamic registration
Static (manifest) registration — declared in AndroidManifest.xml with an <intent-filter>. The receiver can be triggered even when your app is not running (the system starts it). Since Android 8.0 (API 26), most implicit broadcasts can no longer be received via static registration for battery reasons — only a small allowlist of broadcasts (like BOOT_COMPLETED, ACTION_LOCKED_BOOT_COMPLETED, SMS) still work this way.
Dynamic (code) registration — register and unregister programmatically via registerReceiver() / unregisterReceiver(). The receiver only fires while your component is alive. This is the recommended approach for most broadcasts in modern Android. Always unregister in the symmetric lifecycle callback to avoid leaks: register in onStart, unregister in onStop; register in onCreate, unregister in onDestroy.
Broadcast Receivers — dynamic, local, ordered, and custom
// ── Basic BroadcastReceiver ───────────────────────────────────────────────classNetworkReceiver : BroadcastReceiver() {
override funonReceive(context: Context, intent: Intent) {
// Runs on main thread — must be fast (<10 seconds)if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
val isConnected = isNetworkAvailable(context)
// Hand off heavy work immediatelyif (isConnected) WorkManager.getInstance(context).enqueue(syncRequest)
}
}
}
// ── Dynamic registration ──────────────────────────────────────────────────classMainActivity : AppCompatActivity() {
private val networkReceiver = NetworkReceiver()
override funonStart() {
super.onStart()
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(networkReceiver, filter)
}
override funonStop() {
super.onStop()
unregisterReceiver(networkReceiver) // symmetric unregister — MANDATORY
}
}
// ── LocalBroadcastManager — in-process only, more secure, faster// Deprecated in API 29 but still common in interviews. Use Flow/EventBus instead.val localBM = LocalBroadcastManager.getInstance(context)
localBM.registerReceiver(myReceiver, IntentFilter("MY_ACTION"))
localBM.sendBroadcast(Intent("MY_ACTION").apply { putExtra("key", "value") })
// ── Ordered broadcast — receivers run sequentially, can abort chainval resultReceiver = object : BroadcastReceiver() {
override funonReceive(ctx: Context, intent: Intent) {
// Gets final result after all receivers have processedval result = resultData
}
}
sendOrderedBroadcast(Intent("MY_ORDERED_ACTION"), null, resultReceiver, null, Activity.RESULT_OK, null, null)
// ── goAsync() — extend execution time beyond the 10s windowclassHeavyReceiver : BroadcastReceiver() {
override funonReceive(context: Context, intent: Intent) {
val pending = goAsync() // tells system: I’m not done yetCoroutineScope(Dispatchers.IO).launch {
try { doHeavyWork() }
finally { pending.finish() } // MUST call finish() or you leak
}
}
}
💡 Modern alternative to broadcasts
For in-app event communication, prefer SharedFlow or StateFlow over LocalBroadcastManager — they’re lifecycle-aware, type-safe, and don’t require Intent serialisation. For system events like network changes, use ConnectivityManager.registerNetworkCallback() (API 21+) instead of the deprecated CONNECTIVITY_ACTION broadcast.
Content Providers
A Content Provider is Android’s structured mechanism for sharing data between apps. It wraps a data source (typically a Room/SQLite database, but can be files, in-memory structures, or any data store) behind a standard URI-based CRUD interface. The system routes all access through the Content Provider, which enforces permissions, allowing controlled cross-app data sharing.
You interact with other apps’ Content Providers via a ContentResolver, which you get from Context. You don’t talk to the provider directly — the ContentResolver is the broker. Common examples you use without realising: reading contacts (ContactsContract), querying media files (MediaStore), and reading calendar events.
Content Providers — ContentResolver queries and custom provider
You only need to implement a Content Provider if you want to expose your data to other apps. If you’re just storing data for your own app’s use, Room + Repository is sufficient. The most common real-world use of Content Providers is FileProvider for sharing files with the camera or email apps, and reading system providers like Contacts and MediaStore.
Common interview gotchas
viewLifecycleOwner vs this in Fragments
When observing LiveData or Flow in a Fragment, always use viewLifecycleOwner — not this (the Fragment itself). A Fragment can be in the back stack with a destroyed view but an active Fragment lifecycle. If you observe with this, the observer keeps receiving updates and trying to update a null binding. viewLifecycleOwner is tied to the view’s lifecycle and is automatically cancelled in onDestroyView.
Context in ViewModel
Never hold a plain Activity or Fragment context in a ViewModel. The ViewModel outlives configuration changes — the Activity is recreated but the ViewModel persists. Holding the old Activity context leaks it. If you need context in a ViewModel, extend AndroidViewModel which receives the Application context (which is safe to hold), or inject @ApplicationContext via Hilt.
Implicit broadcasts after API 26
Since Android 8.0, apps can no longer register for most implicit broadcasts via the manifest. CONNECTIVITY_CHANGE, BATTERY_CHANGED, and hundreds of others are on the restriction list. Only a small set remain (full list in docs). Dynamic registration still works but only while your app is running. For background network monitoring use ConnectivityManager.registerNetworkCallback(); for scheduled work use WorkManager.
Service runs on the main thread
This is the #1 Service gotcha. Service.onStartCommand() runs on the main thread. Without launching a coroutine or thread, any blocking call (network, disk) ANRs your app. Always start a CoroutineScope(SupervisorJob() + Dispatchers.IO) in the Service and cancel it in onDestroy().
Activity context in a singleton
Passing this (Activity) to a singleton, static field, or long-lived object creates a memory leak. The Activity can never be garbage collected because the singleton holds a strong reference to it. Even after finish(), the entire view hierarchy and all its resources remain in memory. Always pass applicationContext to objects that outlive the Activity.
How these connect to Android architecture
These fundamentals aren’t isolated topics — they compose into every Android architecture interview answer. A typical system design question (“design an offline-first news app”) touches all of them: a single Activity hosts Fragments (one per screen); the Activity lifecycle manages navigation; ViewModels (scoped to the Fragment) survive rotation; Intents handle deep links into specific screens; a Service or WorkManager handles background sync; a BroadcastReceiver (or NetworkCallback) triggers sync when connectivity is restored; Room (accessed via a ContentProvider if sharing with widgets) stores the cache; and Application context wires together the Hilt dependency graph.
The lifecycle callbacks are load-bearing architecture: onResume/onPause guard foreground resources (camera, GPS, sensors); onStart/onStop guard visible-lifetime resources (location updates, animation); onViewCreated/onDestroyView in Fragments guard view bindings. Getting these wrong doesn’t just lose points in an interview — it causes real crashes and battery drain in production.
Fragment lifecycle: adds onAttach / onCreateView / onViewCreated (setup views + observe here!) / onDestroyView (null binding!). Use viewLifecycleOwner for observers, never this.
Intent: Explicit = specify class (within app). Implicit = describe action/data (cross-app). Always check resolveActivity() before implicit. PendingIntent = deferred intent token for notifications/alarms. Use ActivityResultLauncher not startActivityForResult.
Context: Activity context for dialogs/inflation (theme-aware). Application context for singletons/long-lived objects. Never store Activity context in a static field — memory leak.
Services: Started (fire-and-forget, stopSelf). Bound (client-server, IBinder). Foreground (persistent notification, survives low memory). ALL run on main thread — always coroutine off. Prefer WorkManager for deferrable work.
BroadcastReceiver: Static (manifest, API 26+ restrictions). Dynamic (registerReceiver, must unregister). 10-second window — use goAsync() + WorkManager for heavy work. LocalBroadcastManager deprecated — use SharedFlow.
ContentProvider: URI-based CRUD API for cross-app data sharing. Access via ContentResolver (query/insert/update/delete). FileProvider for secure file sharing. Only implement if exposing data to other apps.