BroadcastReceiver, Services, Binder & Bound Services
Android background components that every senior dev must know cold
Open interactive version (quiz + challenge)Real-world analogy
What is it?
BroadcastReceiver, Service, and Binder are three of Android's four core application components. Together they handle background work, event listening, and inter-process communication. Though WorkManager has replaced many Service use cases, these components remain foundational for audio players, location trackers, system integrations, and any app that must react to OS-level events.
Real-world relevance
A music player app uses a Foreground Service to keep playback alive when the user switches apps. It uses a Bound Service so the UI Activity can call play(), pause(), seekTo() directly on the player object. A BroadcastReceiver listens for ACTION_AUDIO_BECOMING_NOISY (headphones unplugged) and pauses playback automatically.
Key points
- BroadcastReceiver — static vs dynamic — Static receivers are declared in AndroidManifest and can receive system broadcasts even when the app is not running (with restrictions in API 26+). Dynamic receivers are registered in code via registerReceiver() and live only while the registering component is alive. Prefer dynamic for most use cases.
- LocalBroadcastManager (legacy awareness) — LocalBroadcastManager was used for in-process broadcasts. It is deprecated since API 26. Modern replacement is LiveData, StateFlow, or EventBus for in-app messaging. Know it exists so you can explain why you would NOT use it today.
- Implicit vs explicit broadcasts — Most implicit system broadcasts (e.g., ACTION_BATTERY_CHANGED) are no longer deliverable to static receivers since API 26 to save battery. Explicit broadcasts (targeting your own app) and a whitelist of critical system broadcasts still work. Mentioning this in interviews signals deep knowledge.
- Started Service — Started with startService() or startForegroundService(). Runs until stopSelf() or stopService() is called. Has no direct communication channel back to the caller. Used for fire-and-forget tasks. Largely replaced by WorkManager for deferrable work.
- Foreground Service — A Service that shows a persistent notification so the user is aware it is running. Required for long-running tasks that need to continue when the app is in the background (e.g., music playback, GPS tracking, file upload). Must call startForeground() within 5 seconds of starting or the system kills it.
- Bound Service — Started with bindService(). Clients bind to it and receive an IBinder to communicate directly. The service lives as long as at least one client is bound. When all clients unbind, the service is destroyed. Used when you need a long-lived object with an API that multiple components can call.
- Binder for same-process IPC — When the Service runs in the same process as the client, you return a concrete Binder subclass from onBind(). The client casts the IBinder to your concrete class and calls methods directly — no serialization overhead. This is the most common pattern.
- AIDL for cross-process IPC — Android Interface Definition Language generates the Binder boilerplate for inter-process communication. You define an .aidl file, Android Studio generates a Stub class, you implement it in the service, and clients use the generated Proxy. Know AIDL exists and when it is needed (different process, system services). For most apps, use Messenger or broadcast instead.
- Messenger — simplified cross-process — Messenger wraps a Handler and uses Binder under the hood. Safer than raw AIDL for simple request-reply patterns across processes. Messages are serialized as Parcelable Bundles. Good middle ground when full AIDL is overkill.
- Service lifecycle gotchas — onStartCommand() return value matters: START_STICKY restarts the service after kill with null intent, START_REDELIVER_INTENT restarts with the original intent, START_NOT_STICKY does not restart. Wrong choice here causes silent reliability bugs in production.
- When to use each in 2026 — Foreground Service: ongoing user-visible work (audio, location). WorkManager: deferrable background work with constraints. BroadcastReceiver: react to system or app events. Bound Service: shared long-lived object within or across processes. Avoid plain started services for anything new.
- Interview framing — Lead with 'In modern Android I reach for WorkManager or Foreground Service + notification. I know BroadcastReceiver and Bound Service deeply because they underpin many system integrations and I've debugged production issues caused by their lifecycle.'
Code example
// Foreground Service example (music player)
class MusicService : Service() {
private val binder = MusicBinder()
private var mediaPlayer: MediaPlayer? = null
inner class MusicBinder : Binder() {
fun getService(): MusicService = this@MusicService
}
override fun onBind(intent: Intent): IBinder = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = buildNotification()
startForeground(NOTIF_ID, notification)
return START_STICKY
}
fun play(url: String) {
mediaPlayer = MediaPlayer().apply {
setDataSource(url)
prepareAsync()
setOnPreparedListener { start() }
}
}
fun pause() { mediaPlayer?.pause() }
fun stop() {
mediaPlayer?.stop()
mediaPlayer?.release()
mediaPlayer = null
stopForeground(true)
stopSelf()
}
override fun onDestroy() {
super.onDestroy()
mediaPlayer?.release()
}
private fun buildNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Now Playing")
.setSmallIcon(R.drawable.ic_music)
.setOngoing(true)
.build()
}
}
// Activity binding
class PlayerActivity : AppCompatActivity() {
private var musicService: MusicService? = null
private var bound = false
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, binder: IBinder) {
musicService = (binder as MusicService.MusicBinder).getService()
bound = true
}
override fun onServiceDisconnected(name: ComponentName) {
bound = false
}
}
override fun onStart() {
super.onStart()
Intent(this, MusicService::class.java).also { intent ->
bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
if (bound) {
unbindService(connection)
bound = false
}
}
}
// Dynamic BroadcastReceiver for headphones unplugged
class NoisyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
// pause playback
}
}
}
// Register dynamically in service
val filter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
registerReceiver(noisyReceiver, filter)Line-by-line walkthrough
- 1. MusicService extends Service and defines an inner MusicBinder class that extends Binder — this is the same-process Binder pattern.
- 2. getService() returns the outer Service instance so the bound client gets a direct reference — no serialization, no IPC overhead.
- 3. onBind() returns the binder instance; Android delivers this to the client's ServiceConnection.onServiceConnected().
- 4. onStartCommand() calls startForeground() immediately with a notification — this is mandatory within 5 seconds of startForegroundService().
- 5. START_STICKY ensures the system restarts the service after killing it to reclaim memory, which is correct for a music player.
- 6. play() creates a MediaPlayer asynchronously using prepareAsync() to avoid blocking the service thread.
- 7. The ServiceConnection in the Activity captures the IBinder, casts it to MusicBinder, and calls getService() — now the Activity has a direct reference to the running Service.
- 8. bindService() in onStart() and unbindService() in onStop() is the correct lifecycle pairing — ensures the service is not kept alive unnecessarily.
- 9. The NoisyReceiver's onReceive() triggers on headphone unplug — the ACTION_AUDIO_BECOMING_NOISY broadcast tells the app the audio route changed.
- 10. Registering the receiver dynamically (in code) instead of the Manifest means it only fires when our service is running — correct scoping.
Spot the bug
class UploadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val fileUri = intent?.getStringExtra("file_uri")
uploadFile(fileUri)
return START_STICKY
}
private fun uploadFile(uri: String?) {
// simulated upload on calling thread
Thread.sleep(30000) // 30 second upload
stopSelf()
}
override fun onBind(intent: Intent): IBinder? = null
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Services overview — Android Developers (Android Docs)
- Bound services overview (Android Docs)
- BroadcastReceiver — Android Developers (Android Docs)
- AIDL — Android Interface Definition Language (Android Docs)
- Foreground services (Android Docs)