Notifications & Permissions
๐ Notifications & Permissions
๐ป Notification Channels โ Required Since Android 8
Every notification must belong to a channel โ a named category the user can individually enable, disable, or customize (sound, vibration, priority) from system settings, independent of every other channel the app has:
Channels exist because a single blanket "allow notifications: yes/no" switch was too coarse โ a user might want message notifications but not promotional ones from the same app. Creating a channel is idempotent (calling createNotificationChannel again with the same ID is a no-op if it already exists), so it's typically done once, early in the app's lifecycle โ often inside the Application class from Course 2 Chapter 5's Hilt setup.
๐จ Building and Showing a Notification
The permission check before .notify(...) isn't optional defensive code โ without it, calling notify without the granted permission throws a SecurityException and crashes the app on Android 13+. This is the bridge into the chapter's second half: showing a notification and requesting permission to do so are two genuinely separate steps.
๐ Runtime Permissions โ Declared Isn't Enough
A "dangerous" permission (camera, location, notifications, and others) must be declared in AndroidManifest.xml and explicitly granted by the user at runtime โ declaring it alone only makes the permission requestable, not automatically granted:
rememberLauncherForActivityResult is Compose's bridge to Android's activity-result APIs โ launcher.launch(...) triggers the actual system permission dialog, and the lambda passed to rememberLauncherForActivityResult receives the user's choice as a plain Boolean, which then drives what the UI does next.
๐ซ Handling Denial Gracefully
A denied permission isn't an error state to hide from โ the app should keep working, just without that specific capability, and should explain why the permission matters before re-asking:
shouldShowRequestPermissionRationale
Returns true if the user denied once but hasn't permanently blocked it โ the signal to show an explanatory message ("We use this to remind you about tasks") before asking again, rather than immediately re-prompting with no context.
Permanently Denied
After a user selects "Don't ask again" (or denies twice on some Android versions), the system stops showing the permission dialog entirely โ the only path forward is directing the user to the app's system settings screen manually.
A denied notification permission shouldn't mean the app is unusable โ sync (last chapter) should still work, tasks should still save; the user simply won't be notified about it. Designing every permission-gated feature as an enhancement layered on top of a working core, rather than a hard requirement, is the difference between a graceful degradation and an app that feels broken the moment someone taps "Deny."
Android Permissions vs Browser Permissions
| Browser (Notification API, Geolocation API) | Android | |
|---|---|---|
| Requesting | Notification.requestPermission() / navigator.geolocation | ActivityResultContracts.RequestPermission() |
| Result | "granted" / "denied" / "default" | A Boolean (granted or not) |
| Re-prompting after denial | Browser-controlled; often can't re-prompt at all from JS | Possible, guided by shouldShowRequestPermissionRationale |
| Permanent block | User must change it in browser site settings | User must change it in Android app settings |
The underlying philosophy is identical to what's likely already familiar from the browser's own geolocation or notification prompts โ the platform, not the app, controls whether and when to ask, and a denial is a real, permanent-feeling answer the app has to respect and design around.
๐ป Coding Challenges
Challenge 1: A Notification Channel and a Notification
Create a notification channel named "task_reminders" with IMPORTANCE_DEFAULT, and a function showTaskReminder(context: Context, taskTitle: String) that builds and shows a notification using that channel, checking the POST_NOTIFICATIONS permission before calling notify().
Goal: Practice the full channel-then-notification setup.
Challenge 2: Requesting the Permission from Compose
Write a composable that checks whether POST_NOTIFICATIONS is already granted (using ContextCompat.checkSelfPermission), and if not, shows a Button that requests it via rememberLauncherForActivityResult, updating a remembered Boolean state based on the result.
Goal: Practice the check-then-request pattern for a runtime permission.
Challenge 3: Graceful Denial Handling
Extend Challenge 2: if the permission is denied, show explanatory text ("Notifications help you stay on top of tasks") only when shouldShowRequestPermissionRationale(activity, permission) returns true, and a different message ("Enable notifications in Settings") when it returns false after a denial. Confirm the rest of the screen (e.g. a placeholder Text saying "Tasks: 3") remains visible regardless of permission state.
Goal: Practice designing a feature that degrades gracefully rather than blocking the whole screen on a denied permission.
Camera, location, contacts, microphone โ every runtime-requestable permission uses the identical check โ request โ handle-denial-gracefully pattern from this chapter, just with a different permission string and a different fallback behavior. Learning this pattern once genuinely transfers to any permission a future feature might need.
๐ฏ What's Next
Next chapter: App Security โ ProGuard/R8, certificate pinning, secure storage, and biometric authentication.