4
Android Architecture — Foundation

Android Core
ComponentsThe four pillars every app is built on

Activity, Service, BroadcastReceiver, and ContentProvider. These are the only entry points into an Android application. Everything else — ViewModels, Fragments, Compose — builds on top of them.

Activity
Service
BroadcastReceiver
ContentProvider
Intents
AndroidManifest
00
The Big Picture
What Are Core Components?

Android apps have no single entry point like a main() function. Instead, the OS can activate any of four distinct component types, in any order, at any time.

Unlike desktop or web apps that start from a single entry point and run linearly, Android is built around a component model. The OS can instantiate any component independently — it can start your app's Service from a notification without ever showing an Activity. It can receive data through a ContentProvider without any UI at all. This design enables deep system integration, inter-app communication, and efficient resource management.

Every component must be declared in the AndroidManifest before the system can use it. The manifest is the contract between your app and the OS. Undeclared components simply don't exist as far as the system is concerned.

INTERACTIVE COMPONENT MAP — click any component to learn more
UI
Background
Events
Data
📱
Activity
UI screens
⚙️
Service
background work
📡
BroadcastReceiver
system events
🗄️
ContentProvider
shared data
Intent
the connector
📋
Manifest
the declaration

SELECT A COMPONENT ABOVE TO SEE ITS ROLE

The key mental model: Think of components as separately addressable objects the OS holds references to. The OS can start any component independently, kill it when resources are needed, and restart it later. Your app does not "run" — specific components are activated when needed and deactivated when done.

01
First Component
Activity

A single, focused screen the user can interact with. The only component that has a user interface and can respond to user input directly.

📱
Activity
android.app.Activity · AppCompatActivity
Purpose
A single screen with a user interface. Represents one coherent task the user is performing — viewing a list, editing a form, watching a video.
Typical lifetime
As long as the user is on this screen. Destroyed when user navigates away or presses Back. Can be recreated on rotation.
Has UI?
Yes — setContentView() inflates the layout. The entire view hierarchy belongs to this Activity's window.
How started
Explicit Intent from another Activity, implicit Intent from any app or system, notification PendingIntent, deep link URL.

The Activity lifecycle — 7 callbacks

Every Activity instance moves through a defined sequence of states. The OS calls these methods to signal transitions. Implementing them correctly is the difference between an app that feels solid and one that leaks memory, loses data, or crashes on rotation.

1
onCreate(savedInstanceState)
Called once when Activity is first created (or after process kill). Initialize UI with setContentView(), set up ViewModels, restore saved state. Heavy work → background thread.
once per instance
2
onStart()
Activity is becoming visible. Register receivers that only work while visible. Not yet interactive.
becoming visible
3
onResume()
Activity is in foreground, receiving user input. Acquire camera, mic, sensors. Resume animations. The "running" state.
★ foreground
4
onPause()
Partially obscured or losing focus. Must be FAST — next Activity cannot start until this returns. Release camera/mic. Save critical lightweight state.
must be fast
5
onStop()
Activity is completely invisible. Release heavy resources (DB connections, location updates). Last guaranteed callback for saves.
invisible
6
onSaveInstanceState()
Save ephemeral UI state to a Bundle before the Activity might be destroyed. Called before onStop. System auto-saves View states if Views have IDs.
before kill
7
onDestroy()
Final cleanup. Check isFinishing() to distinguish user-navigated-away from config change (rotation). NOT guaranteed to be called if process is killed.
final cleanup
MainActivity.kt — production pattern
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by viewModels()
    private var _binding: ActivityMainBinding? = null
    private val binding get() = _binding!!

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        _binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // ViewModel survives rotation — data already loaded
        viewModel.uiState.observe(this) { state -> render(state) }

        // Restore scroll position, selected tab, etc.
        savedInstanceState?.let {
            binding.recycler.scrollToPosition(it.getInt("scroll_pos", 0))
        }
    }

    override fun onResume() {
        super.onResume()
        // Acquire exclusive resources (camera, mic, sensors)
    }

    override fun onPause() {
        super.onPause()
        // FAST — release exclusive resources, save lightweight critical state
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("scroll_pos", binding.recycler.computeVerticalScrollOffset())
    }

    override fun onDestroy() {
        _binding = null  // prevent memory leak
        super.onDestroy()
    }
}

onDestroy is not guaranteed. If the system kills your process to reclaim memory, onDestroy does not fire. Never rely on it for critical data saves. Use onPause or onSaveInstanceState for UI state, and Room/DataStore for persistent data.

Task & back stack — how Activities are managed

Activities are organized into Tasks — stacks of Activities that represent a user's workflow. When you start a new Activity, it's pushed onto the stack. Pressing Back pops it. The app icon in Recents represents one Task. You can control this behavior with launch modes in the manifest.

standard
Default behavior
New instance created every time. Multiple instances of the same Activity can exist in the stack. Most Activities should use this mode.
singleTop
Reuse if on top
If the Activity is already at the top of the stack, reuse it (call onNewIntent). If not at top, create a new instance. Perfect for notification deep links.
singleTask
One instance, clear above
Only one instance exists in the task. If it exists, clear all Activities above it and call onNewIntent. Use for the app's home/root screen.
singleInstance
Private task
The Activity gets its own private task — no other Activity can be added to it. Rare; used for system-level screens like the dialer during a call.
02
Second Component
Service

A component that performs long-running operations in the background with no user interface. Can run even when the user switches to another app.

⚙️
Service
android.app.Service
Purpose
Long-running background operations: playing music, tracking location, syncing data, handling a network connection, processing files.
Has UI?
No — Services run headless. A Foreground Service shows a persistent notification, but that's not the Service's own UI.
Runs on
The main thread by default! Always use a background thread, coroutine, or WorkManager for the actual work inside a Service.
Three types
Started (fire-and-forget), Bound (client-server IPC), Foreground (user-aware, persistent notification).

Three types of Services

Started Service
Fire & forget
Started with startService(). Runs until it calls stopSelf() or another component calls stopService(). Use for one-off background tasks. The modern alternative is WorkManager.
Bound Service
Client-server IPC
Started with bindService(). Offers an interface (IBinder) that other components call. Lives as long as it has at least one client bound. Useful for communicating between app components.
Foreground Service
User-visible work
Shows a persistent notification. The OS treats it with higher priority — much less likely to be killed. Required for: music playback, navigation, fitness tracking, file uploads the user initiated.
MusicPlayerService.kt — Foreground Service
class MusicPlayerService : Service() {

    private val binder = LocalBinder()
    private var mediaPlayer: MediaPlayer? = null

    inner class LocalBinder : Binder() {
        fun getService() = this@MusicPlayerService
    }

    override fun onCreate() {
        super.onCreate()
        mediaPlayer = MediaPlayer()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val notification = buildMediaNotification()
        startForeground(NOTIFICATION_ID, notification)  // promote to foreground
        mediaPlayer?.start()
        return START_STICKY  // restart if killed, re-deliver last intent
    }

    override fun onBind(intent: Intent): IBinder = binder

    override fun onDestroy() {
        mediaPlayer?.stop()
        mediaPlayer?.release()
        mediaPlayer = null
        super.onDestroy()
    }
}

Service vs WorkManager: For most background work that doesn't need user-visibility, prefer WorkManager. It handles process death, battery optimization (Doze mode), network constraints, and retries automatically. Use a Service directly only for long-running user-initiated work that needs a foreground notification, or for IPC between components.

START_STICKY, START_NOT_STICKY, START_REDELIVER_INTENT

The return value of onStartCommand() tells Android what to do if the process is killed while the Service is running. This is the restart policy:

START_STICKY
Restart, no intent
Service is restarted after being killed. Intent delivered to onStartCommand is null. Use for services that manage their own state and don't need the original intent (music player, location tracker).
START_NOT_STICKY
Don't restart
Service is NOT restarted if killed. Use for services that do work triggered by intents and that work can be rescheduled (sync operations that WorkManager handles).
START_REDELIVER_INTENT
Restart with intent
Service is restarted AND the last intent is re-delivered. Use when you must not lose the work request — file download, upload, or any operation where the original intent is critical.
03
Third Component
BroadcastReceiver

A dormant component that wakes up to handle broadcast messages from the system or other apps, then goes back to sleep. Runs for 10 seconds max.

📡
BroadcastReceiver
android.content.BroadcastReceiver
Purpose
Respond to system-wide events: boot completed, network change, battery low, screen on/off, incoming call. Also receives broadcasts from other apps.
Time limit
10 seconds maximum in onReceive(). After this, the OS considers it unresponsive. Never do I/O or network calls here directly.
Has UI?
No — but it can start an Activity, Service, or post a notification. It cannot show a dialog directly (use NotificationManager instead).
Registered via
Manifest (for boot-time or implicit broadcasts) or dynamically with registerReceiver() (for runtime-only listening, tied to an Activity's lifecycle).

Manifest vs Dynamic registration

Manifest registration
Static — always listening
Declared in AndroidManifest.xml. The app does not need to be running — Android can launch it to handle the broadcast. Restricted since Android 8: implicit broadcasts can no longer be received via manifest (with few exceptions like BOOT_COMPLETED).
Dynamic registration
Runtime — lifecycle-bound
Registered in code with registerReceiver(), unregistered with unregisterReceiver(). Only active while the component is alive. Symmetric: register in onStart, unregister in onStop. Works for all broadcast types including implicit.
BroadcastReceiver — patterns
// ── Manifest-registered (AndroidManifest.xml) ─────────────────
// <receiver android:name=".BootReceiver" android:exported="false">
//   <intent-filter>
//     <action android:name="android.intent.action.BOOT_COMPLETED"/>
//   </intent-filter>
// </receiver>

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // Schedule WorkManager jobs that run on every boot
            WorkManager.getInstance(context).enqueue(OneTimeWorkRequest...)
            // NEVER do long work here — 10 second limit
        }
    }
}

// ── Dynamic registration (in Activity) ────────────────────────
class MainActivity : AppCompatActivity() {
    private val networkReceiver = object : BroadcastReceiver() {
        override fun onReceive(ctx: Context, intent: Intent) {
            viewModel.onNetworkChanged()
        }
    }

    override fun onStart() {
        super.onStart()
        val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
        registerReceiver(networkReceiver, filter)
    }
    override fun onStop() {
        unregisterReceiver(networkReceiver)  // always symmetric
        super.onStop()
    }
}

// ── goAsync() — for work that needs slightly more time ─────────
class DataReceiver : BroadcastReceiver() {
    override fun onReceive(ctx: Context, intent: Intent) {
        val pendingResult = goAsync()  // extends to ~30s, still not for I/O
        CoroutineScope(Dispatchers.Default).launch {
            processData(intent)
            pendingResult.finish()  // must call finish or system kills it
        }
    }
}

Android 8+ background restrictions: Most implicit broadcasts can no longer be received via the manifest. Your receiver simply won't fire. The solution is dynamic registration (while the app is open) or WorkManager constraints (to respond to network, battery, etc. changes while app is in background). A small list of exceptions (like BOOT_COMPLETED) still work via manifest.

04
Fourth Component
ContentProvider

A structured interface for sharing data between apps — or exposing internal app data to the system. The only component designed specifically for cross-app data access.

🗄️
ContentProvider
android.content.ContentProvider
Purpose
Exposes structured data through a standard URI-based interface. Used by Contacts, MediaStore, FileProvider, and any app that shares data across process boundaries.
URI format
content://authority/path/id — e.g., content://contacts/people/1. Authority uniquely identifies your provider across the system.
Access pattern
Queries via ContentResolver — a system service that routes requests to the appropriate provider. Works across processes via Binder IPC transparently.
When to use
Sharing data with other apps, file access via FileProvider, exposing search suggestions, sync adapters. Not needed for app-internal data — use Room/DataStore directly.
ContentProvider — six required methods
class BookProvider : ContentProvider() {

    private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH).apply {
        addURI("com.example.books", "books", 1)     // all books
        addURI("com.example.books", "books/#", 2)  // specific book by ID
    }

    override fun onCreate(): Boolean {
        // Initialize database. Called on main thread — be fast.
        return true
    }

    override fun query(uri: Uri, projection: Array<String>?, selection: String?,
                        selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
        return when (uriMatcher.match(uri)) {
            1 -> db.query("books", projection, selection, selectionArgs, null, null, sortOrder)
            2 -> db.query("books", projection, "_id=?", arrayOf(uri.lastPathSegment!!), null, null, null)
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? { ... }
    override fun update(uri: Uri, values: ContentValues?, sel: String?, selArgs: Array<String>?): Int = 0
    override fun delete(uri: Uri, sel: String?, selArgs: Array<String>?): Int = 0
    override fun getType(uri: Uri): String? = when (uriMatcher.match(uri)) {
        1 -> "vnd.android.cursor.dir/vnd.example.books"
        2 -> "vnd.android.cursor.item/vnd.example.books"
        else -> null
    }
}

// ── Accessing via ContentResolver (any app) ────────────────────
val cursor = contentResolver.query(
    Uri.parse("content://com.example.books/books"),
    arrayOf("_id", "title", "author"),
    null, null, "title ASC"
)
cursor?.use { c ->
    while (c.moveToNext()) {
        val title = c.getString(c.getColumnIndexOrThrow("title"))
    }
}

FileProvider — safe file sharing

The most common real-world use of ContentProvider is FileProvider — sharing files with other apps (camera, email, share sheet) without exposing your app's file system. FileProvider is a ContentProvider subclass provided by AndroidX that handles all the URI generation and permission granting automatically.

FileProvider — share a file safely
// AndroidManifest.xml
// <provider android:name="androidx.core.content.FileProvider"
//     android:authorities="${applicationId}.fileprovider"
//     android:exported="false" android:grantUriPermissions="true">
//   <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
//       android:resource="@xml/file_paths"/>
// </provider>

// res/xml/file_paths.xml
// <paths> <cache-path name="images" path="images/"/> </paths>

// In code — share a file
val file = File(cacheDir, "images/photo.jpg")
val uri = FileProvider.getUriForFile(this, "${packageName}.fileprovider", file)

val shareIntent = Intent(Intent.ACTION_SEND).apply {
    type = "image/jpeg"
    putExtra(Intent.EXTRA_STREAM, uri)
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)  // grants read to receiver
}
startActivity(Intent.createChooser(shareIntent, "Share image"))
05
Communication Layer
Intents & Intent Filters

The message system that connects all components together. Intents are the lingua franca of Android — every cross-component communication flows through them.

An Intent is a messaging object used to request an action from another component. It can carry data (action, data URI, type, extras, flags) and target a specific component (explicit) or let the system find the best match (implicit).

EXPLICIT INTENT — direct addressing
HomeActivitysender
Intent(this, DetailActivity::class.java)explicit intent
DetailActivitytarget — direct
IMPLICIT INTENT — system routes to best match
YourAppsender
Intent(ACTION_VIEW, Uri("geo:..."))implicit intent
Android OSroutes via filters
Google Mapsbest match
Intent anatomy — all fields
// ── Explicit Intent ────────────────────────────────────────────
val explicit = Intent(this, DetailActivity::class.java).apply {
    putExtra("product_id", "prod_42")           // extras — arbitrary key-value data
    putExtra("show_reviews", true)
    addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)    // behavior flags
}
startActivity(explicit)

// ── Implicit Intent — open URL ─────────────────────────────────
val viewUrl = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
startActivity(viewUrl)  // browser or WebView app handles this

// ── Implicit Intent — share text ───────────────────────────────
val share = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"                          // MIME type narrows the match
    putExtra(Intent.EXTRA_TEXT, "Check this out!")
}
startActivity(Intent.createChooser(share, "Share via"))

// ── Always check if an implicit intent can be handled ──────────
if (viewUrl.resolveActivity(packageManager) != null) {
    startActivity(viewUrl)   // safe — at least one app can handle it
} else {
    showMessage("No app available to handle this action")
}

// ── Intent Filter (in manifest — receiving implicit intents) ───
// <activity android:name=".ShareReceiver">
//   <intent-filter>
//     <action android:name="android.intent.action.SEND"/>
//     <category android:name="android.intent.category.DEFAULT"/>
//     <data android:mimeType="image/*"/>
//   </intent-filter>
// </activity>

PendingIntent — deferred execution

A PendingIntent wraps an Intent and grants another app (typically the system) the right to fire it on your app's behalf, at a future time. Used for notifications, AlarmManager, and widget actions.

PendingIntent — notification tap action
val intent = Intent(this, MainActivity::class.java).apply {
    putExtra("notification_id", notifId)
    addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
val pendingIntent = PendingIntent.getActivity(
    this, requestCode, intent,
    PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)

val notification = NotificationCompat.Builder(this, CHANNEL_ID)
    .setContentTitle("New message")
    .setContentIntent(pendingIntent)  // fires when notification is tapped
    .setAutoCancel(true)
    .build()
06
The Contract
AndroidManifest.xml

The app's contract with the Android OS. Every component, permission, and hardware requirement the app needs must be declared here before the OS will honor them.

AndroidManifest.xml — complete anatomy
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <!-- ① Permissions — declared before use ─────────────────── -->
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.CAMERA"/>
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

    <!-- ② Hardware features ──────────────────────────────────── -->
    <uses-feature android:name="android.hardware.camera" android:required="false"/>

    <application
        android:name=".MyApplication"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_launcher"
        android:theme="@style/Theme.App"
        android:allowBackup="true">

        <!-- ③ Activity ───────────────────────────────────────── -->
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
            <!-- Deep link -->
            <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="myapp.com"/>
            </intent-filter>
        </activity>

        <!-- ④ Service ────────────────────────────────────────── -->
        <service
            android:name=".MusicPlayerService"
            android:exported="false"
            android:foregroundServiceType="mediaPlayback"/>

        <!-- ⑤ BroadcastReceiver ─────────────────────────────── -->
        <receiver
            android:name=".BootReceiver"
            android:exported="false">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

        <!-- ⑥ ContentProvider ───────────────────────────────── -->
        <provider
            android:name=".BookProvider"
            android:authorities="${applicationId}.books"
            android:exported="false"/>

    </application>
</manifest>

Key manifest attributes

AttributeApplies toMeaning
android:exportedAll componentsWhether other apps can start this component. Must be explicitly declared on API 31+. Default false for Services and Receivers. Must be true for any component with an intent-filter that other apps use.
android:launchModeActivitystandard, singleTop, singleTask, singleInstance. Controls back stack and instance creation behavior.
android:permissionAll componentsCallers must hold this permission to start/bind the component. Enforced by the OS — no code required in the component itself.
android:processAll componentsRun this component in a separate process. Each process gets its own VM and memory space. Colons prefix = private; no colon = shared with other apps.
android:foregroundServiceTypeServiceRequired for foreground services on Android 10+. Values: camera, microphone, location, mediaPlayback, phoneCall, dataSync, etc.
android:authoritiesContentProviderUnique identifier for the provider. Must be globally unique — use your package name as a prefix. Used in content:// URIs.
android:grantUriPermissionsContentProviderAllows the provider to grant temporary read/write URI permissions to other apps. Required for FileProvider.
07
Modern Android
Modern Patterns

How the four core components relate to the modern Android development ecosystem — what's changed, what's superseded, and what remains essential.

The four core components have not been replaced — they are still the only entry points the OS uses. But the implementation patterns inside each component have evolved dramatically. Fragments, ViewModel, Compose, WorkManager, and Jetpack Navigation all build on top of the component model rather than replacing it.

Activity → Single Activity pattern
One Activity, many screens
Modern apps use a single Activity (or very few) with Jetpack Navigation managing the fragment/Compose back stack. The Activity becomes a container — it handles system integrations (permissions, back press, window insets) while screens live as Fragments or composable destinations.
Service → WorkManager / Coroutines
Background work modernized
For most background work, WorkManager is preferred — it handles process death, battery optimization, network constraints, and retries. For user-initiated long-running work (music, download), Foreground Service + coroutines remains the right approach.
BroadcastReceiver → restricted
Implicit broadcasts limited
Since Android 8, most implicit broadcasts cannot be received via manifest. Dynamic registration works while the app is open. For responding to conditions while in background, use WorkManager constraints (network, battery, storage) instead of broadcast receivers.
ContentProvider → FileProvider
File sharing is the main use case
Most apps don't need a custom ContentProvider anymore — Room handles local data, APIs handle remote data. The main use case is FileProvider for safe file sharing. If you do need to expose data to other apps, ContentProvider is still the right tool.
Modern single-Activity setup with Navigation Component
// Single Activity — container for all navigation destinations
class MainActivity : AppCompatActivity() {

    private val binding by viewBinding(ActivityMainBinding::inflate)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        // NavHostFragment manages all screen navigation
        // Each screen is a Fragment or Compose NavHost destination
        // Activity only handles: permissions, back press, window insets
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        // Deep link handling — NavController routes to the right screen
        findNavController(R.id.nav_host).handleDeepLink(intent)
    }
}

// activity_main.xml
// <FragmentContainerView android:name="androidx.navigation.fragment.NavHostFragment"
//     app:navGraph="@navigation/nav_graph"
//     app:defaultNavHost="true"/>

// ── With Compose — NavHost replaces FragmentContainerView ──────
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val navController = rememberNavController()
            AppNavHost(navController = navController)  // all screens in Compose
        }
    }
}

The component model survives all of this. Jetpack Compose doesn't replace Activities — it runs inside them. ViewModels don't replace Services — they run within the Activity lifecycle. Navigation Component doesn't replace the OS task back stack — it adds a fragment/compose layer on top. The four components remain the foundation everything else builds on.

08
Reference
Full Comparison

A complete decision matrix for choosing the right component for any Android development scenario.

Property Activity Service BroadcastReceiver ContentProvider
Has UI?YesNoNoNo
Started byIntent (explicit/implicit), launcher, notificationstartService(), bindService(), systemsendBroadcast(), system eventsContentResolver queries
Runs on main thread?YesYes — use coroutines!Yes — max 10 secondsNo — caller's thread
Time limitNone (user-driven)None (foreground); background limited by OS10 seconds strictlyNo strict limit but should be fast
Survives app background?No (stopped, may be killed)Yes (foreground); maybe (background)Briefly (10s)Yes (process independent)
Needs manifest declaration?YesYesYes (static) or No (dynamic)Yes
Cross-app access?Yes (exported=true)Yes (exported=true)Yes (implicit intents)Yes (primary use case)
Process kill impactRecreated from saved state bundleRestarted per START_ policyLost if not yet startedRecreated when queried
Modern alternativeStill required (Compose inside it)WorkManager (most cases)WorkManager constraintsFileProvider / Room
Best used forEvery screen — the core UI unitMusic, nav, downloads, FG workBoot, alarms, cross-app eventsFile sharing, system integration
The four components are the only entry points into your app.

Everything else in the Android ecosystem — ViewModel, Fragment, Compose, WorkManager, Navigation — builds on top of these four. Master the components and you understand Android from the ground up.