Broadcast Receivers

A BroadcastReceiver is a component that listens for and reacts to broadcast messages – one-way announcements that something happened. Broadcasts can come from the system (battery low, connectivity changed, device booted) or from your own app (a recipe was just added, a sync finished).

Unlike a Service, a BroadcastReceiver doesn't keep running – it wakes up briefly to handle one event and then it's done. That makes it a good fit for RecipeBox to react to things like "the battery is low, so pause any background photo uploads" without keeping a whole component alive just to wait for that moment.

Extending BroadcastReceiver

Every receiver overrides a single method, onReceive(), which is called when a matching broadcast arrives:

// BatteryLowReceiver.java
public class BatteryLowReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        if (Intent.ACTION_BATTERY_LOW.equals(intent.getAction())) {
            // Pause background recipe-photo uploads to save power
            RecipePhotoUploadQueue.getInstance(context).pauseAll();
            Log.d("RecipeBox", "Battery low – paused photo uploads");
        }
    }
}
Explanation:
  • onReceive() runs on the main thread and must return quickly – no long-running work here.
  • Checking intent.getAction() lets one receiver handle multiple broadcast types if needed.
  • Intent.ACTION_BATTERY_LOW is a system broadcast the OS sends when the battery is critically low.

Static vs. Dynamic Registration

A receiver only fires once it's registered somewhere. There are two ways to do that:

RegistrationWhere declaredLifetimeBest for
Static (manifest) <receiver> tag in AndroidManifest.xml Can be triggered even if the app process isn't running A shrinking list of broadcasts still allowed to wake up an app (e.g. BOOT_COMPLETED)
Dynamic (code) registerReceiver() in an Activity/Service/Application Only active while that component is alive; must call unregisterReceiver() to stop it Most system broadcasts today, plus any broadcast you only care about while the UI is visible
Modern restriction: to save battery and reduce background wake-ups, recent Android versions no longer deliver most implicit system broadcasts (like connectivity changes) to receivers declared statically in the manifest. For the vast majority of system broadcasts today, you must register the receiver dynamically at runtime instead – it only listens while your app has a live component to host it.
// Registering dynamically, e.g. in onStart()
private final BatteryLowReceiver batteryReceiver = new BatteryLowReceiver();

@Override
protected void onStart() {
    super.onStart();
    IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_LOW);
    registerReceiver(batteryReceiver, filter);
}

@Override
protected void onStop() {
    super.onStop();
    unregisterReceiver(batteryReceiver);
}
Explanation: the receiver is registered when the Activity becomes visible and unregistered when it's no longer visible – this avoids leaking a receiver that keeps listening after nobody needs it.

Custom Broadcasts Within Your Own App

You can also broadcast events that only your own app cares about – for example, telling every screen in RecipeBox that "a new recipe was just added" so a list can refresh itself.

// Sending a custom broadcast after saving a recipe
public static final String ACTION_RECIPE_ADDED = "com.recipebox.RECIPE_ADDED";

public void onRecipeSaved(Recipe recipe) {
    Intent intent = new Intent(ACTION_RECIPE_ADDED);
    intent.putExtra("recipeId", recipe.getId());
    context.sendBroadcast(intent);
}
// A receiver elsewhere in the app listening for it
public class RecipeAddedReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        long recipeId = intent.getLongExtra("recipeId", -1);
        // e.g. refresh the RecyclerView showing the recipe list
        recipeListViewModel.reload();
    }
}
A note on LocalBroadcastManager: it used to be the standard way to send broadcasts that never leave your own app, but it is now deprecated. For same-app communication, a simpler in-app callback, a shared LiveData/ViewModel (see ViewModel & LiveData), or an event-bus-style pattern is usually a better fit than any kind of broadcast – reserve real BroadcastReceivers for events that genuinely originate outside your own components, like system broadcasts.

Declaring a Static Receiver (When You Need One)

For the few broadcasts still allowed to wake an app via the manifest, declare the receiver like this:

<receiver
    android:name=".BootReceiver"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

Troubleshooting

Receiver never fires? → For most system broadcasts, confirm it's registered dynamically, not just declared in the manifest – newer Android versions ignore manifest-only registrations for implicit broadcasts.

App crashes with "Receiver not registered"? → You called unregisterReceiver() twice, or the Activity was destroyed before you unregistered; always pair registration with the matching lifecycle callback.

Custom broadcast not received by another screen? → Make sure the action string matches exactly, and that the listening receiver is registered before the broadcast is sent.

Reacting to a single event is one thing; making something happen at a specific future time is another. Continue to AlarmManager & JobScheduler next.