Lesson 33 of 83 intermediate

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

A NotificationChannel is like a radio station — you create it once (Classic Rock, News, Sports), and users tune in or tune out per channel from their settings. Each notification is a broadcast on that station. Deep links are like street addresses — anyone can navigate to your specific screen if they know the address. App Links are verified addresses — they come with an official deed (Digital Asset Links) proving you own the building.

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

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. 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. 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. 3. TaskStackBuilder.addNextIntentWithParentStack() reads the parentActivityName from the manifest to build the correct back stack automatically.
  4. 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. 5. NotificationCompat.Builder uses the channel ID string — if the channel doesn't exist on Android 8+, the notification is silently dropped.
  6. 6. BigTextStyle.bigText() shows expanded text when the user pulls down the notification — the regular contentText shows in the collapsed state.
  7. 7. setAutoCancel(true) dismisses the notification when tapped — without this, the notification remains in the tray after the user taps it.
  8. 8. android:autoVerify='true' in the intent filter triggers Android's automatic domain verification — without it, the link shows a disambiguation dialog.
  9. 9. handleDeepLink() is called from both onCreate() and onNewIntent() — onCreate handles cold start, onNewIntent handles warm start (Activity already in back stack).
  10. 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. 11. Build.VERSION.SDK_INT >= TIRAMISU check prevents requesting POST_NOTIFICATIONS on older Android versions where the permission doesn't exist.
  12. 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?
Check channel creation timing, required notification fields, PendingIntent flags, notification ID collision, and App Links intent filter completeness.
Show answer
Bug 1: The notification channel 'order_updates' is never created before posting — on Android 8+ this silently drops the notification. Fix: call NotificationManagerCompat.createNotificationChannel() or NotificationManager.createNotificationChannel() at app startup (Application.onCreate) before any notification is posted. Bug 2: setSmallIcon() is missing — this is a required field. A notification without a small icon will throw an exception on some Android versions and will never display. Fix: add .setSmallIcon(R.drawable.ic_order) to the builder. Bug 3: PendingIntent flags are 0 — on Android 12+ this throws IllegalArgumentException because PendingIntents must specify FLAG_IMMUTABLE or FLAG_MUTABLE. Fix: use PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE. Bug 4: All order notifications use the same hardcoded ID (1) — if two orders ship simultaneously, the second notification silently replaces the first. Fix: use orderId.hashCode() as the notification ID to give each order a unique notification. Bug 5: The intent filter for App Links is missing android:autoVerify='true' and is missing the BROWSABLE category — without autoVerify, Android never verifies domain ownership and shows a disambiguation dialog instead of opening the app directly. Fix: add android:autoVerify='true' to the intent-filter element and add <category android:name='android.intent.category.BROWSABLE'/> inside the filter.

Explain like I'm 5

A notification channel is like a TV channel — your app creates a 'Sports' channel and a 'News' channel. Users can mute the News channel in their TV settings but keep Sports on. Deep links are like shortcuts — someone texts you 'go to room 42' and you know exactly where to go. App Links are like a VIP pass — the app has officially proven it owns that web address, so your phone skips asking and just opens the app.

Fun fact

Android verifies App Links by making an HTTP request to your domain's /.well-known/assetlinks.json file — not during app install, but asynchronously in the background. If the verification fails (404, wrong certificate, HTTP instead of HTTPS), Android falls back silently to showing the disambiguation dialog. You can check verification status with: adb shell pm get-app-links --user cur com.yourpackage

Hands-on challenge

Build a production notification system: (1) Create three channels with appropriate importance levels: ORDER_UPDATES (HIGH), PROMOTIONS (LOW), SYSTEM_ALERTS (DEFAULT); (2) A function that builds a notification for an order update with a BigTextStyle expanded view, two action buttons ('Track Order' and 'Contact Support'), each with their own deep link PendingIntent, and correct TaskStackBuilder back stack; (3) Handle the deep link in an Activity supporting both cold start (onCreate) and warm start (onNewIntent); (4) Write the assetlinks.json content for your domain; (5) Request POST_NOTIFICATIONS on Android 13+ with rationale shown on denial.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery