Notifications & Channels (Java)
Notifications let RecipeBox reach a user even when the app isn't open — a reminder to try a saved recipe tonight, or a heads-up that a background sync is running. Starting a new section on user-facing integrations, this lesson covers the modern, channel-based notification system every Android app targeting recent API levels must use.
Notification Channels Are Required (API 26+)
Since Android 8.0 (API level 26), every notification must be posted to a Notification Channel. A channel groups related notifications under one label the user can control independently — muting "Recipe Reminders" without silencing a "Sync Status" channel from the same app, for example. You create channels once, typically in your Application subclass, and Android ignores the call entirely on older OS versions where channels don't exist.
// RecipeBoxApp.java
public class RecipeBoxApp extends Application {
public static final String CHANNEL_REMINDERS = "recipe_reminders";
@Override
public void onCreate() {
super.onCreate();
createNotificationChannels();
}
private void createNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_REMINDERS,
"Recipe Reminders",
NotificationManager.IMPORTANCE_DEFAULT
);
channel.setDescription("Reminders to try a saved recipe");
NotificationManager manager = getSystemService(NotificationManager.class);
manager.createNotificationChannel(channel);
}
}
}
- The channel ID ("recipe_reminders") is a stable string you reuse whenever you post a notification to this channel.
- IMPORTANCE_DEFAULT controls the default sound/visibility behavior; users can still override it in system settings.
- createNotificationChannel() is safe to call repeatedly — creating a channel that already exists with the same ID is a no-op.
Building a Notification with NotificationCompat.Builder
With a channel in place, posting an actual notification uses NotificationCompat.Builder, which works consistently across old and new API levels. This example reminds the user to try tonight's suggested recipe, and tapping it opens RecipeBox directly to that recipe's detail screen via a PendingIntent:
// Showing a reminder for a Recipe the user saved
public void showRecipeReminder(Context context, Recipe recipe) {
Intent intent = new Intent(context, RecipeDetailActivity.class);
intent.putExtra("recipe_id", recipe.getId());
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(
context, recipe.getId(), intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, RecipeBoxApp.CHANNEL_REMINDERS)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Time to cook!")
.setContentText("Try " + recipe.getTitle() + " tonight")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true);
NotificationManagerCompat.from(context).notify(recipe.getId(), builder.build());
}
- setContentIntent() wires the tap action; setAutoCancel(true) dismisses the notification once tapped.
- The notification ID passed to notify() — here, the recipe's own id — lets you later update or cancel that exact notification.
- FLAG_IMMUTABLE is required on API 31+ for PendingIntents that don't need to be modified by the receiving component.
The POST_NOTIFICATIONS Runtime Permission (API 33+)
Starting with Android 13 (API level 33), posting any notification requires the POST_NOTIFICATIONS runtime permission, requested the same way as camera or location access described in Runtime Permissions. Without it, calls to notify() silently do nothing on API 33+ devices.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_CODE_NOTIFICATIONS);
}
}
Expandable Styles
A single-line notification truncates longer text. Attaching a NotificationCompat.BigTextStyle lets the user expand it (by pulling down the notification shade) to read the full ingredient list or instructions:
builder.setStyle(new NotificationCompat.BigTextStyle()
.bigText("Ingredients: " + String.join(", ", recipe.getIngredients())));
Foreground Service Notifications Are Persistent
The Services & Foreground Service lesson's sync service must show a notification the entire time it runs — this is what makes it a foreground service rather than a background one the system can kill freely. That notification behaves differently from the reminder above: it cannot be swiped away while the service is active, since it exists specifically to tell the user work is in progress.
// Inside RecipeSyncService, shown while syncing runs
Notification syncNotification = new NotificationCompat.Builder(this, CHANNEL_SYNC)
.setSmallIcon(R.drawable.ic_sync)
.setContentTitle("Syncing RecipeBox")
.setContentText("Updating your saved recipes...")
.setOngoing(true)
.build();
startForeground(NOTIFICATION_ID_SYNC, syncNotification);