📱 Core Concept · ~45 min read · Beginner → Advanced

Core Android Fundamentals

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

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 (onCreateonDestroy), the visible lifetime (onStartonStop), and the foreground lifetime (onResumeonPause).

📋 Activity lifecycle — state machine with all seven callbacks
Activity Launched onCreate() Inflate layout, init ViewModel, setup binding onStart() Activity visible. Start UI-bound work. onResume() ▶ RUNNING — user interacting onPause() Partially hidden. Pause animations, sensors. onStop() Fully hidden. Release heavy resources. onDestroy() Final cleanup. Release all resources. Activity Destroyed onRestart() Coming back from Stopped Another activity finishes Entire lifetime: onCreate → onDestroy Visible lifetime: onStart → onStop Foreground: onResume → onPause

What to do in each callback

CallbackWhat happensDo hereDon’t do here
onCreateActivity created (or recreated after kill)Inflate layout, set binding, init ViewModel, setup RecyclerViewHeavy I/O, network calls directly
onStartActivity visible but not interactiveRegister UI-bound listeners, start animations that need visibilityCamera, GPS (needs foreground)
onResumeActivity in foreground, user can interactStart camera, resume playback, register sensors, request locationSlow operations (blocks UI thread)
onPauseActivity losing focus (dialog/nav appearing)Pause camera, pause playback, release sensors, commit unsaved dataSlow saves, heavy operations (must be fast)
onStopActivity fully hiddenRelease heavy resources, cancel non-critical network requests, save stateNothing that must survive process death
onDestroyActivity being torn down permanently or recreatedFinal 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
class ProductActivity : AppCompatActivity() { private val vm: ProductViewModel by viewModels() private lateinit var binding: ActivityProductBinding override fun onCreate(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 fun onResume() { super.onResume() // Start things that need the foreground: camera, sensors, location } override fun onPause() { super.onPause() // MUST be fast — next activity’s onResume blocks until this returns // Release camera, pause media player } override fun onStop() { super.onStop() // Activity fully hidden — release heavy resources } override fun onSaveInstanceState(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
Activity Fragment What to do onAttach() Context available. Get args/Activity reference. onCreate() Init ViewModel, restore arguments. NO view yet. onCreateView() Inflate and return the Fragment’s view. onViewCreated() ⭐ Best place: setup RecyclerView, click listeners, observe ViewModel, bind views. onStart() onStart() onResume() onResume() ▶ onPause() onPause() onStop() onStop() onDestroyView() ⭐ Null your binding reference here! View is gone but Fragment may still exist.
⚠️ 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
class ProductFragment : Fragment(R.layout.fragment_product) { private var _binding: FragmentProductBinding? = null private val binding get() = _binding!! // safe to use between onCreateView and onDestroyView private val vm: ProductViewModel by viewModels() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, state: Bundle?): View { _binding = FragmentProductBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(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 fun onDestroyView() { super.onDestroyView() _binding = null // prevent memory leak — MANDATORY } }

Fragment vs Activity — when to use each

ActivityFragment
RepresentsA full screen / app entry pointA reusable portion of a screen
NavigationIntent-based, separate back stack per taskFragmentManager back stack, Navigation component
Sharing dataVia Intent extras or SharedViewModel (complex)Shared ViewModel trivial across Fragments in same Activity
ReuseLow — heavyweight, owns a windowHigh — same Fragment in different screens / tablets
Modern guidanceSingle-Activity app preferredNavigation 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).

📋 Intents — explicit vs implicit resolution flow
Explicit Intent Intent(this, DetailActivity::class.java) You name the exact component class. DetailActivity launched Direct. Used within your own app. Implicit Intent Intent(Intent.ACTION_VIEW, Uri.parse(url)) Describe action + data. System finds handler. System checks <intent-filter> in manifests action + category + data must all match Browser 1 match → opens Multiple → chooser No match → ActivityNotFoundException — always check resolveActivity()!
Intents — explicit, implicit, PendingIntent, ActivityResultLauncher
// Explicit Intent — within your app, you know the target val 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 handler val 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 intents if (shareIntent.resolveActivity(packageManager) != null) { startActivity(Intent.createChooser(shareIntent, "Share via")) } // Common implicit intents // Open URL Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com")) // Dial phone Intent(Intent.ACTION_DIAL, Uri.parse("tel:+91-9999999999")) // Send email Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:support@example.com")) // ActivityResultLauncher — modern replacement for startActivityForResult private 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, Shortcuts val pendingIntent = PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.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 (abstract) ContextWrapper Application ContextThemeWrapper Service Activity ★ Application Context ✓ Resources, strings, files ✓ startService, sendBroadcast ✗ Show dialogs ✗ Themed UI Activity Context ✓ Everything Application can do ✓ Show dialogs, inflate views ✓ Themed resources (correct theme) ⚠ Memory Leak Risk Holding Activity context in a singleton or static field keeps the Activity alive even after finish() — use Application ctx.
Context — when to use which, and avoiding leaks
// Application context — lives as long as the app process // Safe to store in singletons, repositories, ViewModels class ImageCache(private val context: Context) { // ✔ Pass applicationContext when creating singletons private val diskCache = DiskCache(context.applicationContext) } // Activity context — tied to that Activity’s lifecycle // Use for: inflating views, showing dialogs, accessing themed resources val dialog = AlertDialog.Builder(this) // ✔ Activity context — dialog needs a window val inflater = LayoutInflater.from(this) // ✔ Activity context — correct theme // ⚠ Common leak: storing Activity context in a singleton object BadSingleton { var context: Context? = null // ✗ If you store an Activity here, it never GC’d } // Fix: store applicationContext instead object GoodSingleton { lateinit var appContext: Context fun init(context: Context) { appContext = context.applicationContext } } // getSystemService — get OS-level services via Context val connectivityManager = context.getSystemService(ConnectivityManager::class.java) val notificationManager = context.getSystemService(NotificationManager::class.java) val inputMethodManager = context.getSystemService(InputMethodManager::class.java)
Use caseContext to useWhy
Inflate a layout / ViewActivityNeeds the Activity’s theme for correct styling
Show an AlertDialogActivityDialog must attach to an Activity’s window
Room database / Retrofit singletonApplicationLives forever; Activity context would leak
WorkManager / AlarmManagerApplicationMay fire when Activity is gone
getResources / getStringEitherActivity gives theme-aware resources
startActivity from ViewModelApplication + FLAG_NEW_TASKViewModel 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
Started Service startService() → onCreate → onStartCommand Runs until stopSelf() or stopService() Independent of calling component Use: fire-and-forget background work onStartCommand return values: START_STICKY — restart, null intent START_NOT_STICKY — don’t restart START_REDELIVER_INTENT — restart + intent Bound Service bindService() → onCreate → onBind() Returns IBinder for direct method calls Destroyed when all clients unbind Use: client-server, music controls, IPC Client calls: bindService(intent, conn, flags) onServiceConnected — get IBinder onServiceDisconnected — handle crash unbindService(conn) when done Foreground Service startForeground(id, notification) Shown to user via persistent notification System won’t kill when memory low Use: music, navigation, file download API 26+: must call startForeground() within 5 seconds of start API 34: must declare foregroundServiceType in manifest (<service android:foregroundServiceType>) Services run on main thread by default — always offload I/O to coroutines or threads! For deferred/periodic work prefer WorkManager over started services — it survives process death and handles retries.
Services — started, bound, and foreground patterns
// ── Started Service ──────────────────────────────────────────────────────── class SyncService : Service() { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { scope.launch { performSync() } // offload to coroutine — service is on main thread! return START_NOT_STICKY // don’t restart after kill } override fun onBind(intent: Intent?) = null // not a bound service override fun onDestroy() { scope.cancel() } } // ── Bound Service ───────────────────────────────────────────────────────── class MusicService : Service() { inner class MusicBinder : Binder() { fun getService() = this@MusicService } private val binder = MusicBinder() override fun onBind(intent: Intent?): IBinder = binder fun play(track: String) { /* start playback */ } } // In Activity/Fragment: private var musicService: MusicService? = null private val conn = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, binder: IBinder) { musicService = (binder as MusicService.MusicBinder).getService() } override fun onServiceDisconnected(name: ComponentName) { musicService = null } } // ── Foreground Service ──────────────────────────────────────────────────── class DownloadService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification = buildNotification() startForeground(1, notification) // must call within 5s on API 26+ return START_STICKY } override fun onBind(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 ─────────────────────────────────────────────── class NetworkReceiver : BroadcastReceiver() { override fun onReceive(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 immediately if (isConnected) WorkManager.getInstance(context).enqueue(syncRequest) } } } // ── Dynamic registration ────────────────────────────────────────────────── class MainActivity : AppCompatActivity() { private val networkReceiver = NetworkReceiver() override fun onStart() { super.onStart() val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) registerReceiver(networkReceiver, filter) } override fun onStop() { 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 chain val resultReceiver = object : BroadcastReceiver() { override fun onReceive(ctx: Context, intent: Intent) { // Gets final result after all receivers have processed val result = resultData } } sendOrderedBroadcast(Intent("MY_ORDERED_ACTION"), null, resultReceiver, null, Activity.RESULT_OK, null, null) // ── goAsync() — extend execution time beyond the 10s window class HeavyReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val pending = goAsync() // tells system: I’m not done yet CoroutineScope(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
// ── ContentResolver — querying another app’s provider ────────────────────── // URI structure: content://authority/path/id // ^ ^ ^ // identifies provider table row // Query contacts (requires READ_CONTACTS permission) val cursor: Cursor? = contentResolver.query( ContactsContract.Contacts.CONTENT_URI, // URI arrayOf(ContactsContract.Contacts.DISPLAY_NAME), // projection (columns) null, // selection (WHERE clause) null, // selectionArgs "${ContactsContract.Contacts.DISPLAY_NAME} ASC" // sortOrder ) cursor?.use { // use {} auto-closes the cursor while (it.moveToNext()) { val name = it.getString(it.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME)) contacts.add(name) } } // CRUD operations via ContentResolver // Insert val values = ContentValues().apply { put("name", "Alice"); put("age", 30) } val newUri = contentResolver.insert(MyProvider.CONTENT_URI, values) // Update contentResolver.update(MyProvider.CONTENT_URI, values, "id = ?", arrayOf("1")) // Delete contentResolver.delete(MyProvider.CONTENT_URI, "id = ?", arrayOf("1")) // ── Custom ContentProvider ──────────────────────────────────────────────── class ProductProvider : ContentProvider() { companion object { const val AUTHORITY = "com.example.app.provider" val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/products") } override fun onCreate(): Boolean { /* init database */; return true } override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? { return db.query("products", projection, selection, selectionArgs, null, null, sortOrder) } override fun getType(uri: Uri): String = "vnd.android.cursor.dir/vnd.$AUTHORITY.products" override fun insert(uri: Uri, values: ContentValues?): Uri? = null override fun update(uri: Uri, values: ContentValues?, selection: String?, args: Array<String>?): Int = 0 override fun delete(uri: Uri, selection: String?, args: Array<String>?): Int = 0 } // FileProvider — share private app files with other apps securely (API 24+) // In AndroidManifest.xml: // <provider android:name="androidx.core.content.FileProvider" // android:authorities="${applicationId}.fileprovider" // android:exported="false" android:grantUriPermissions="true"> val photoUri = FileProvider.getUriForFile(context, "${packageName}.fileprovider", photoFile) val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, photoUri) addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) }
✅ When you actually need a Content 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.

🎯 Interview Summary

Activity lifecycle: onCreate (inflate, ViewModel) → onStart (visible) → onResume (foreground, camera/GPS) → onPause (release foreground, FAST) → onStop (release heavy) → onDestroy (final cleanup). ViewModel survives rotation; onSaveInstanceState survives process death.

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.