DEV Community

Cover image for Day 2/100: The 4 Android Components — What Senior Engineer Get Wrong
Hoang Son
Hoang Son

Posted on

Day 2/100: The 4 Android Components — What Senior Engineer Get Wrong

This is Day 2 of my 100 Days to Senior Android Engineer series. Each post follows a consistent format: what I thought I knew → what I actually learned → interview implications.


🔍 The concept

Android has exactly four fundamental application components:

  • Activity — a single screen with a UI
  • Service — background work with no UI
  • BroadcastReceiver — responds to system-wide or app-wide events
  • ContentProvider — structured data sharing between apps

Every Android developer learns these in week one. What separates junior from senior isn't knowing the list — it's understanding the boundaries and the cost of each one.


💡 What I thought I knew

After years of building apps, my mental model was roughly:

  • Need a screen? → Activity (or Fragment)
  • Need background work? → Service
  • Need to react to a system event? → BroadcastReceiver
  • Need to share data with other apps? → ContentProvider

Simple enough. And for most day-to-day work, this is fine.

The problem is that this mental model is when to use them, not what they actually are — and the distinction matters enormously when things break.


😳 What I actually learned

Each component is a system entry point — not just a class

This is the insight that reframes everything.

When you declare a component in AndroidManifest.xml, you're not just registering a class. You're telling the Android system: "here is a door into my app." The system can open that door independently of your app's current state — even if no other part of your app is running.

This has real consequences:

User taps notification
    → System creates new Activity (entry point #1)

Device boots, BOOT_COMPLETED fires
    → System wakes your BroadcastReceiver (entry point #2)

Another app queries your ContentProvider
    → System starts your process just for the ContentProvider (entry point #3)
Enter fullscreen mode Exit fullscreen mode

Your Application.onCreate() runs before all of these. Which means every entry point carries Application initialization cost — and if that initialization is slow or throws, it affects all of them.


The BroadcastReceiver trap most seniors fall into

// Registered in Manifest — looks innocent
class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // DON'T do this
        val db = Room.databaseBuilder(context, AppDatabase::class.java, "db").build()
        db.someDao().doHeavyWork() // runs on main thread, no coroutine scope
    }
}
Enter fullscreen mode Exit fullscreen mode

onReceive() runs on the main thread and has a hard time limit of ~10 seconds before the system ANRs. There is no coroutine scope. There is no lifecycle. The receiver is considered dead the moment onReceive() returns.

The correct pattern for anything non-trivial:

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Hand off immediately — don't do work here
        SyncWorker.enqueue(context)
    }
}
Enter fullscreen mode Exit fullscreen mode

Delegate to WorkManager and return. That's it.


Service is not what you think in 2024

Service runs on the main thread by default. Not a background thread. Not a coroutine. The main thread.

class MyService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // This is still the main thread
        // Heavy work here = ANR
        return START_STICKY
    }
}
Enter fullscreen mode Exit fullscreen mode

The distinction that matters now:

Type Visibility Use case
Service (background) None Legacy. Avoid on API 26+. System kills it aggressively.
ForegroundService Persistent notification Music playback, navigation, active uploads
WorkManager None Deferrable, guaranteed background work
CoroutineWorker None WorkManager + coroutines, the modern default

Since API 26 (Android 8.0), background services are killed almost immediately when your app goes to the background. If you're still using plain Service for background processing, you're relying on behavior that the system actively works against.


ContentProvider is the most misunderstood of the four

Most developers think: "I don't share data with other apps, so I don't need ContentProvider."

But here's what they're missing — ContentProvider initializes before Application.onCreate().

Process starts
    → ContentProviders init (in manifest order)
    → Application.onCreate()
    → Activity/Service/Receiver entry point
Enter fullscreen mode Exit fullscreen mode

Libraries like WorkManager, Firebase, and Jetpack Startup exploit this deliberately. They ship a ContentProvider with nothing inside — just an onCreate() — to auto-initialize without requiring you to add code to your Application class.

// From Jetpack App Startup — the entire trick
class InitializationProvider : ContentProvider() {
    override fun onCreate(): Boolean {
        // Runs before Application.onCreate()
        // Perfect for library auto-init
        AppInitializer.getInstance(context!!).discoverAndInitialize()
        return true
    }
    // query, insert, etc. — all throw UnsupportedOperationException
}
Enter fullscreen mode Exit fullscreen mode

If you've ever wondered how a library "just works" without any setup code — this is usually how.


🧪 The mental model that stuck

Think of each component as having a lifecycle owner and a thread contract:

Component          | Lifecycle owner      | Default thread  | Survives process death?
-------------------|----------------------|-----------------|------------------------
Activity           | User (navigates)     | Main            | No (state via Bundle)
Fragment           | Activity / BackStack | Main            | No (state via Bundle)
Service            | You (stopSelf)       | Main (!)        | Maybe (START_STICKY)
ForegroundService  | You + notification   | Main (!)        | Yes, while notif shows
BroadcastReceiver  | Single onReceive()   | Main            | No — dies immediately
ContentProvider    | Process lifetime     | Binder thread   | Yes, while process lives
WorkManager        | System scheduler     | Background      | Yes, survives reboot
Enter fullscreen mode Exit fullscreen mode

When something breaks — a crash, an ANR, unexpected behavior after process death — I now ask: which entry point triggered this, and what are its thread and lifecycle contracts?

That single question resolves about 60% of the confusion.


❓ The interview question

Here's the version of this question that trips people up at the senior level:

"Your app needs to sync data when the device finishes charging. Walk me through the component choices and the tradeoffs of each approach."

The naive answer: BroadcastReceiver for ACTION_BATTERY_CHANGED.

The senior answer considers:

  • Is this deferrable? (Yes → WorkManager with requiresCharging(true))
  • What if the sync takes > 10 seconds? (Rules out BroadcastReceiver body entirely)
  • What if the user force-stops the app? (WorkManager handles re-scheduling; a plain Receiver doesn't)
  • What's the battery and data impact? (Constraints API)
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS)
    .setConstraints(
        Constraints.Builder()
            .setRequiresCharging(true)
            .setRequiredNetworkType(NetworkType.UNMETERED)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "periodic_sync",
    ExistingPeriodicWorkPolicy.KEEP,
    syncRequest
)
Enter fullscreen mode Exit fullscreen mode

The question isn't testing if you know WorkManager exists. It's testing whether you reason about constraints, reliability, and system behavior — not just "which class do I instantiate."


What surprised me revisiting this

I thought I knew these components cold. What I actually found:

  1. I'd internalized "Service for background work" without regularly questioning whether I needed a Service at all in 2024
  2. I'd never thought carefully about ContentProvider initialization order — and now I see it everywhere in library source code
  3. The "entry point" framing changed how I read crash reports. When I see a crash in a component I didn't expect to be running, I now know to look for which entry point woke up the process

Tomorrow

Day 3 → Activity Lifecycle — the diagram you've seen a hundred times, and the three scenarios it doesn't show you.

Found a gap in your own understanding? Drop it in the comments — I read every one.


Day 1: Going Back to Basics as a Senior Android Dev

Top comments (0)