Notifications, Channels, Push Flows, Deep Links & App Links
Drive users back into your app with precision and intent
Open interactive version (quiz + challenge)Real-world analogy
What is it?
Notifications re-engage users when the app is not in focus. Channels give users granular control over notification categories. Deep links and App Links route external URLs directly to specific screens within your app, enabling seamless flows from push notifications, emails, marketing campaigns, and shared content.
Real-world relevance
In Hazira Khata school management app, three notification channels were created: Attendance Alerts (HIGH importance), Fee Reminders (DEFAULT), and General Announcements (LOW). FCM data messages built notifications with deep link PendingIntents to the specific student's profile. App Links on the school's domain meant clicking a shared attendance report URL opened the app directly without a browser detour.
Key points
- NotificationChannel (Android 8+) — Channels must be created before posting any notification. createNotificationChannel() is idempotent — safe to call on every app start. Each channel has its own importance level, sound, vibration, and can be controlled by the user independently in Settings.
- Notification importance levels — IMPORTANCE_HIGH (heads-up, sound), IMPORTANCE_DEFAULT (sound, no heads-up), IMPORTANCE_LOW (no sound), IMPORTANCE_MIN (silent, no status bar icon). Set at channel creation — users can lower but not raise importance. Choose conservatively to preserve user trust.
- NotificationCompat.Builder — Always use NotificationCompat (not Notification.Builder) for backward compatibility. Key fields: setSmallIcon (required), setContentTitle, setContentText, setContentIntent (PendingIntent for tap), setAutoCancel(true) (dismiss on tap), setStyle for expanded layouts (BigTextStyle, InboxStyle, MessagingStyle).
- PendingIntent for notification tap — The PendingIntent opens the correct screen when the user taps the notification. Use TaskStackBuilder to build a proper back stack so the user doesn't get stranded on the target screen with no back navigation.
- FCM notification vs data messages — Notification messages: system handles display when app is background/killed (no code needed). Data messages: always delivered to onMessageReceived(). A combined message behaves as a notification message when backgrounded. For maximum control, use data-only messages and build notifications yourself.
- Deep links with custom scheme — myapp://product/123 — define an intent filter with action.VIEW, category.DEFAULT, category.BROWSABLE, and data scheme='myapp'. Handle in Activity via intent.data URI parsing. Works from any app, email, SMS, but browsers may warn users about unknown schemes.
- App Links (HTTP/HTTPS verified) — https://teamzlab.com/product/123 — same as deep links but with the autoVerify='true' attribute on the intent filter. Android verifies ownership by fetching /.well-known/assetlinks.json from your domain. Verified links open directly in your app with no disambiguation dialog.
- assetlinks.json — Must be hosted at https://yourdomain.com/.well-known/assetlinks.json. Contains your app's package name and SHA-256 signing certificate fingerprint. Get fingerprint via: keytool -list -v -keystore release.jks. Must be served with Content-Type: application/json.
- Handling deep link in Activity — In onCreate() and onNewIntent(): val uri = intent.data ?: return. Parse uri.pathSegments to extract parameters. For Jetpack Navigation, use NavController.handleDeepLink(intent) or define deepLinks in the nav graph XML.
- NavDeepLink in Compose/Navigation — In the nav graph: deepLinks = listOf(navDeepLink { uriPattern = "https://teamzlab.com/product/{id}" }). The {id} is automatically extracted as a NavArgument. Use NavController.navigate(deepLinkUri) programmatically from notification PendingIntent.
- Notification POST_NOTIFICATIONS permission (Android 13+) — Android 13 (API 33) requires POST_NOTIFICATIONS permission — it's a runtime permission like CAMERA. Request it with ActivityResultContracts.RequestPermission(). Without it, no notifications are shown for your app. In Hazira Khata school app, this was a critical onboarding step.
- Deep link intent flags — When launching an Activity from a notification deep link, use FLAG_ACTIVITY_SINGLE_TOP to avoid stacking duplicate Activities. Combine with TaskStackBuilder for correct back navigation. FLAG_ACTIVITY_CLEAR_TOP removes all instances above the target in the back stack.
Code example
// 1. Create notification channels on app start (idempotent)
fun createNotificationChannels(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channels = listOf(
NotificationChannel(
"attendance_alerts",
"Attendance Alerts",
NotificationManager.IMPORTANCE_HIGH
).apply { description = "Real-time attendance notifications" },
NotificationChannel(
"fee_reminders",
"Fee Reminders",
NotificationManager.IMPORTANCE_DEFAULT
).apply { description = "Monthly fee reminders" }
)
val nm = context.getSystemService(NotificationManager::class.java)
channels.forEach { nm.createNotificationChannel(it) }
}
}
// 2. Build notification with deep link PendingIntent
fun showAttendanceNotification(context: Context, studentId: String, name: String) {
// Build back stack: Home -> StudentDetail
val stackBuilder = TaskStackBuilder.create(context).apply {
addNextIntentWithParentStack(
Intent(context, StudentDetailActivity::class.java).apply {
data = Uri.parse("https://hazirakhata.com/student/$studentId")
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
)
}
val pendingIntent = stackBuilder.getPendingIntent(
0, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(context, "attendance_alerts")
.setSmallIcon(R.drawable.ic_attendance)
.setContentTitle("Attendance Marked")
.setContentText("$name marked absent today")
.setStyle(NotificationCompat.BigTextStyle()
.bigText("$name was marked absent at 9:15 AM. Tap to view full report."))
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build()
NotificationManagerCompat.from(context)
.notify(studentId.hashCode(), notification)
}
// 3. App Links intent filter in AndroidManifest.xml
/*
<activity android:name=".StudentDetailActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"
android:host="hazirakhata.com"
android:pathPrefix="/student/"/>
</intent-filter>
</activity>
*/
// 4. Handle deep link in Activity
class StudentDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
handleDeepLink(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleDeepLink(intent)
}
private fun handleDeepLink(intent: Intent) {
val uri = intent.data ?: return
val studentId = uri.lastPathSegment ?: return
viewModel.loadStudent(studentId)
}
}
// 5. Request POST_NOTIFICATIONS on Android 13+
val requestPermission = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
if (!granted) showPermissionRationale()
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermission.launch(Manifest.permission.POST_NOTIFICATIONS)
}Line-by-line walkthrough
- 1. createNotificationChannel() is idempotent — calling it multiple times with the same ID does nothing, so calling it in Application.onCreate() every launch is safe and recommended.
- 2. IMPORTANCE_HIGH enables heads-up notifications (pop-up banner) and sound — use only for time-sensitive, user-requested alerts to avoid channel importance being degraded by the user.
- 3. TaskStackBuilder.addNextIntentWithParentStack() reads the parentActivityName from the manifest to build the correct back stack automatically.
- 4. PendingIntent.FLAG_IMMUTABLE is required on Android 12+ — always combine with FLAG_UPDATE_CURRENT so the PendingIntent's extras are updated if re-created.
- 5. NotificationCompat.Builder uses the channel ID string — if the channel doesn't exist on Android 8+, the notification is silently dropped.
- 6. BigTextStyle.bigText() shows expanded text when the user pulls down the notification — the regular contentText shows in the collapsed state.
- 7. setAutoCancel(true) dismisses the notification when tapped — without this, the notification remains in the tray after the user taps it.
- 8. android:autoVerify='true' in the intent filter triggers Android's automatic domain verification — without it, the link shows a disambiguation dialog.
- 9. handleDeepLink() is called from both onCreate() and onNewIntent() — onCreate handles cold start, onNewIntent handles warm start (Activity already in back stack).
- 10. uri.lastPathSegment extracts the last path component — for /student/123 it returns '123'. For complex URLs use uri.getQueryParameter(key) for query params.
- 11. Build.VERSION.SDK_INT >= TIRAMISU check prevents requesting POST_NOTIFICATIONS on older Android versions where the permission doesn't exist.
- 12. NotificationManagerCompat.from(context).notify(id, notification) — using studentId.hashCode() as the notification ID groups updates for the same student.
Spot the bug
fun showOrderNotification(context: Context, orderId: String) {
// Bug 1 — channel not created before posting
val notification = NotificationCompat.Builder(context, "order_updates")
.setContentTitle("Order Shipped!")
.setContentText("Your order #$orderId is on the way")
// Bug 2 — missing small icon
.setContentIntent(
PendingIntent.getActivity(
context, 0,
Intent(context, OrderActivity::class.java).apply {
putExtra("order_id", orderId)
},
0 // Bug 3
)
)
.build()
NotificationManagerCompat.from(context).notify(1, notification) // Bug 4
}
// AndroidManifest.xml
/*
<activity android:name=".ProductActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https"
android:host="myapp.com"
android:pathPrefix="/product/"/>
</intent-filter> Bug 5
</activity>
*/Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Create and Manage Notification Channels (Android Docs)
- Android App Links — Verification Guide (Android Docs)
- Create Deep Links to App Content (Android Docs)
- FCM Message Types (Firebase Docs)
- Notification Permission (POST_NOTIFICATIONS) Best Practices (Android Docs)