Manifest vs Runtime Broadcast Receivers
What is a Broadcast Receiver?
A broadcast receiver is an Android component that listens for system-wide or app-specific broadcast events. When a matching broadcast is sent, Android wakes up the receiver and delivers the intent.
Two Registration Methods
1. Manifest-declared receivers
Registered in AndroidManifest.xml. The system starts the receiver when the broadcast occurs, even if the app is not running.
<receiver
android:name=".BootReceiver"
android:enabled="true"
android:exported="true">
<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 periodic work on boot
WorkManager.getInstance(context)
.enqueueUniqueWork(
"BootSync",
ExistingWorkPolicy.KEEP,
OneTimeWorkRequestBuilder<BootSyncWorker>().build()
)
}
}
}
2. Runtime-registered receivers
Registered in code using registerReceiver(). Only active while the registering component is alive.
class NetworkActivity : AppCompatActivity() {
private val networkReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val isConnected = intent.getBooleanExtra(
ConnectivityManager.EXTRA_NO_CONNECTIVITY, false
)
updateUI(isConnected)
}
}
override fun onStart() {
super.onStart()
val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
registerReceiver(networkReceiver, filter)
}
override fun onStop() {
super.onStop()
unregisterReceiver(networkReceiver)
}
}
Key Differences
| Feature | Manifest | Runtime |
|---|---|---|
| Lifecycle | App process | Component lifecycle |
| When active | Any time (app killed or not) | Only while component is alive |
| Use case | Boot completed, SMS received | Connectivity, battery changes |
| Android 8.0+ | Most implicit broadcasts removed | Preferred method |
Android 8.0+ Restrictions
Android 8.0 removed most implicit broadcasts from manifest-declared receivers. Only a whitelist of broadcasts can still be declared in the manifest:
ACTION_BOOT_COMPLETEDACTION_LOCALE_CHANGEDACTION_TIMEZONE_CHANGEDACTION_POWER_CONNECTED/ACTION_POWER_DISCONNECTEDACTION_SHUTDOWN
All other implicit broadcasts must be registered at runtime.
System Broadcasts and Best Practices
Common System Broadcasts
| Broadcast | Action | Notes |
|---|---|---|
| Boot completed | ACTION_BOOT_COMPLETED |
Manifest only |
| Connectivity | CONNECTIVITY_ACTION |
Runtime only (Android 7.0+) |
| Battery low | ACTION_BATTERY_LOW |
Runtime only |
| Power connected | ACTION_POWER_CONNECTED |
Manifest or runtime |
| Screen on/off | ACTION_SCREEN_ON / ACTION_SCREEN_OFF |
Runtime only |
| Time zone changed | ACTION_TIMEZONE_CHANGED |
Manifest or runtime |
Exported Receivers
For Android 12+, explicitly set android:exported on all receivers:
<!-- Only your app can trigger this -->
<receiver
android:name=".InternalReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.myapp.INTERNAL_ACTION" />
</intent-filter>
</receiver>
<!-- External apps can trigger this -->
<receiver
android:name=".BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
Android 13+ Runtime Permission
For runtime receivers listening to ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED, or similar package-related broadcasts, you need the QUERY_ALL_PACKAGES permission or a specific <queries> declaration in the manifest.
LocalBroadcastManager (Deprecated)
LocalBroadcastManager was used for app-internal broadcasts that never leave the process. It is deprecated because StateFlow/SharedFlow and LiveData are better alternatives:
// Old approach (deprecated)
LocalBroadcastManager.getInstance(context)
.registerReceiver(receiver, IntentFilter("INTERNAL_EVENT"))
// Modern approach — use Flow
class EventBus {
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events
suspend fun emit(event: Event) {
_events.emit(event)
}
}
Best Practices
- Always unregister runtime receivers — unregister in
onStop()oronDestroy()to avoid memory leaks. - Use exported=false — unless external apps must trigger the receiver.
- Use explicit intents — for app-internal broadcasts, avoid implicit broadcasts that could be intercepted.
- Check intent action — always verify
intent.actionbefore processing. - Prefer Flow over LocalBroadcastManager — for type-safe, lifecycle-aware communication.
Quiz
1. Why did Android 8.0 remove most implicit broadcasts from manifest-declared receivers?
2. Which broadcast can still be declared in the manifest on Android 8.0+?
3. What is the recommended modern replacement for LocalBroadcastManager?
4. When should you use a runtime-registered receiver instead of a manifest-declared one?
Flashcards
Question
What is the difference between manifest-declared and runtime-registered broadcast receivers?
Click to reveal answer
Answer
Manifest receivers are active even when the app is not running. Runtime receivers are only active while the registering component (Activity, Service) is alive.
Question
Why did Android 8.0 restrict implicit broadcasts from manifest receivers?
Click to reveal answer
Answer
To reduce battery drain and memory usage. Apps would all wake up simultaneously for common broadcasts like connectivity changes, causing unnecessary resource consumption.
Question
What should you always do with a runtime-registered receiver?
Click to reveal answer
Answer
Unregister it in onStop() or onDestroy() to prevent memory leaks. The receiver holds a reference to the registering component.
Question
What does `android:exported="true"` mean on a broadcast receiver?
Click to reveal answer
Answer
External apps can send broadcasts that trigger this receiver. Set it to false unless you need external apps to communicate with your receiver.
Revision Notes
Key Takeaways
- 1. Android 8.0+ removed most implicit broadcasts from manifest receivers to save battery
- 2. Runtime receivers must be unregistered to avoid memory leaks
- 3. LocalBroadcastManager is deprecated — use Flow or LiveData instead
- 4. Always set android:exported explicitly on receivers for Android 12+ compatibility
Interview Tips
- • Explain the Android 8.0 broadcast restriction and its motivation — battery optimization
- • Know the whitelist of broadcasts still allowed in the manifest
- • Be ready to explain why LocalBroadcastManager is deprecated and what to use instead
- • Mention that runtime receivers are lifecycle-bound — a common source of bugs
Cheat Sheet
Broadcast Receivers Cheat Sheet
Two Registration Methods:
- Manifest: Active when app is not running (most implicit broadcasts removed in Android 8.0+)
- Runtime: Active only while component is alive
Android 8.0+ Whitelist (Manifest OK):
- BOOT_COMPLETED
- LOCALE_CHANGED
- TIMEZONE_CHANGED
- POWER_CONNECTED / POWER_DISCONNECTED
- SHUTDOWN
Best Practices:
- Always unregister runtime receivers
- Use exported=false unless external trigger needed
- Use explicit intents for app-internal broadcasts
- Prefer Flow over LocalBroadcastManager (deprecated)
Common System Broadcasts:
- CONNECTIVITY_ACTION → runtime only
- BATTERY_LOW → runtime only
- BOOT_COMPLETED → manifest or runtime
- SCREEN_ON / SCREEN_OFF → runtime only
Android 13+:
- QUERY_ALL_PACKAGES needed for package-related broadcasts