Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
📱 Android App (Kotlin + XML)  ·  Lesson 26

Notifications, Errors & Offline

Order notification

9:41▮▮ 85%🛒 KartEasy · nowOrder placed 🎉Your order KE261019C8F96E of₹2,023.20 has been placed. Tap totrack it.9:41▮▮ 85%←Order details⋮Order #KE261019C8F96EPlaced✔ Placed ○ Confirmed ○ Shipped ○DeliveredSmart Watch ProQty 1 × ₹1,899Learn Kotlin in 30 D…Qty 1 × ₹349Total amount₹2,023.20
Tapping the notification opens that order directly

Step 1: channel and permission

Since Android 8 every notification belongs to a channel the user can switch off in settings. We create the "Order updates" channel in KartApp.onCreate(). Since Android 13 the app must also ask for POST_NOTIFICATIONS; if the user says no, the app works normally without notifications.

Kotlin – notify/Notifier.kt
package com.codingeasily.karteasy.notify

import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.navigation.NavDeepLinkBuilder
import com.codingeasily.karteasy.MainActivity
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.ui.orders.OrderDetailFragment

/** Local notifications, e.g. "Order placed". Tapping it opens that order's detail screen. */
object Notifier {

    private const val CHANNEL_ORDERS = "orders"

    fun createChannels(context: Context) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ORDERS,
                context.getString(R.string.channel_orders),
                NotificationManager.IMPORTANCE_DEFAULT
            ).apply { description = context.getString(R.string.channel_orders_desc) }
            context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
        }
    }

    fun orderPlaced(context: Context, orderId: Int, orderNo: String, total: String) {
        // Android 13+: without the permission we simply skip the notification.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
            ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
        ) return

        // Deep link straight into the Navigation graph: MainActivity -> orderDetailFragment
        val pendingIntent = NavDeepLinkBuilder(context)
            .setComponentName(MainActivity::class.java)
            .setGraph(R.navigation.nav_main)
            .setDestination(R.id.orderDetailFragment)
            .setArguments(bundleOf(OrderDetailFragment.ARG_ORDER_ID to orderId))
            .createPendingIntent()

        val notification = NotificationCompat.Builder(context, CHANNEL_ORDERS)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(context.getString(R.string.notif_order_title))
            .setContentText(context.getString(R.string.notif_order_text, orderNo, total))
            .setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notif_order_text, orderNo, total)))
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .setColor(ContextCompat.getColor(context, R.color.brand))
            .build()

        NotificationManagerCompat.from(context).notify(orderId, notification)
    }
}
Kotlin – MainActivity.kt (permission)
// MainActivity.kt – ask once on Android 13+ (the result is not required: the app works without it)
private val askNotifications = registerForActivityResult(ActivityResultContracts.RequestPermission()) { }

private fun askNotificationPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
        ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
    ) {
        askNotifications.launch(Manifest.permission.POST_NOTIFICATIONS)
    }
}
XML – drawable/ic_notification.xml (small white icon)
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:tint="?attr/colorControlNormal"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="#FFFFFFFF"
        android:pathData="M7,18c-1.1,0 -1.99,0.9 -1.99,2S5.9,22 7,22s2,-0.9 2,-2 -0.9,-2 -2,-2zM1,2v2h2l3.6,7.59 -1.35,2.45c-0.16,0.28 -0.25,0.61 -0.25,0.96 0,1.1 0.9,2 2,2h12v-2H7.42c-0.14,0 -0.25,-0.11 -0.25,-0.25l0.03,-0.12 0.9,-1.63h7.45c0.75,0 1.41,-0.41 1.75,-1.03l3.58,-6.49C21.25,4.16 20.78,3.5 20.1,3.5H5.21l-0.94,-2H1zM17,18c-1.1,0 -1.99,0.9 -1.99,2s0.89,2 1.99,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>

NavDeepLinkBuilder creates a PendingIntent that starts MainActivity and navigates straight to orderDetailFragment with the order id. It also builds a sensible back stack (Home underneath), so Back from the order goes to Home.

Kotlin – CheckoutFragment.kt (where it is called)
// CheckoutFragment.kt – right after the API confirms the order
is CheckoutViewModel.Event.OrderPlaced -> {
    val total = Format.price(e.order.total)
    Notifier.orderPlaced(requireContext(), e.order.orderId, e.order.orderNo, total)
    findNavController().navigate(R.id.action_checkout_to_success, bundleOf(/* … */))
}
Notification tapPendingIntentMainActivitysplash → setupUi()NavHostreads the deep linkorderDetailFragmentorderId = 1
From the notification to the order screen

Error handling everywhere

Every screen follows the same three rules, so errors never crash the app and the user always knows what to do:

safeCallevery API call returns Result; no exception escapesUiState.Errorfull-screen error with "Try again" when a screen cannot loadSnackbarsmall actions (add to cart, wishlist) show the API messageField errorsvalidation shown under the TextInputLayout401 handlingexpired token → back to login automatically
Five layers of friendly errors
SituationUser sees
Home fails to loadcloud icon, "No internet connection…", Try again
Out of stock while addingSnackbar "Only 3 left in stock."
Wrong couponred text under the coupon field
Token expiredlogin screen opens
Next page fails while scrollingSnackbar; the loaded items stay

Offline: banner + cache

9:41▮▮ 85%☰KartEasy⋮You are offline. Showing saveddata.Festive Salefrom the cacheMobilesFashionBooks⌚Smart Watch Pro₹1,899🎧Earbuds X2₹1,099🏠Home🗂️Categories♡Wishlist🛒Cart👤Account
Airplane mode: the banner appears and Home still shows the last data

Step 3: NetworkMonitor

A ConnectivityManager.NetworkCallback updates a StateFlow<Boolean> whenever the connection appears or disappears. MainActivity shows the banner; the OkHttp interceptor uses isOnline().

Kotlin – util/NetworkMonitor.kt
package com.codingeasily.karteasy.util

import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow

/** Tells the app whether the phone is online, and updates live when that changes. */
class NetworkMonitor(context: Context) {

    private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    private val _online = MutableStateFlow(isOnline())
    val online: StateFlow<Boolean> = _online.asStateFlow()

    private val callback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            _online.value = true
        }

        override fun onLost(network: Network) {
            _online.value = isOnline()
        }
    }

    init {
        val request = NetworkRequest.Builder()
            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
            .build()
        cm.registerNetworkCallback(request, callback)
    }

    fun isOnline(): Boolean {
        val caps = cm.getNetworkCapabilities(cm.activeNetwork) ?: return false
        return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }
}
Kotlin – MainActivity.kt (banner)
// MainActivity.kt – the grey "You are offline" strip under the toolbar
launch {
    app.network.online.collect { online -> binding.offlineBanner.visibleIf(!online) }
}

Step 4: OkHttp offline cache

OkHttp has a 10 MB disk cache. The trick is to always use the network when online (so the cart and prices are never stale) but still store a copy, and use the copy only when offline:

Kotlin – ApiClient.kt (offline part)
// ApiClient.kt (offline part)
// Online : always ask the server (FORCE_NETWORK) so prices, stock and the cart are fresh,
//          but OkHttp still saves a copy of the response on disk.
// Offline: answer GET requests from that saved copy (up to 7 days old).
val offline = Interceptor { chain ->
    val request = chain.request()
    if (request.method != "GET") return@Interceptor chain.proceed(request)
    val policy = if (network.isOnline()) CacheControl.FORCE_NETWORK
    else CacheControl.Builder().onlyIfCached().maxStale(7, TimeUnit.DAYS).build()
    chain.proceed(request.newBuilder().cacheControl(policy).build())
}

// Our PHP API sends no cache headers. This network interceptor marks GET responses
// as storable ("public" is needed because the request has an Authorization header).
val cacheResponses = Interceptor { chain ->
    val response = chain.proceed(chain.request())
    if (chain.request().method == "GET" && response.isSuccessful) {
        response.newBuilder().header("Cache-Control", "public, max-age=0").removeHeader("Pragma").build()
    } else response
}

Online

  • FORCE_NETWORK
  • fresh answer from PHP
  • copy saved to disk

Offline

  • onlyIfCached + maxStale 7 days
  • answer from disk
  • never cached → error with Retry
GET requests only — POST always needs the server

Test checklist

  1. 1Place an order → notification "Order placed 🎉" → tap → the order opens
  2. 2Deny the notification permission → ordering still works, just no notification
  3. 3Open Home, then airplane mode → banner appears, pull to refresh still shows Home
  4. 4Open a product you never opened while offline → error state with Try again
  5. 5Turn internet back on → banner disappears, Try again loads
  6. 6Delete the token row in phpMyAdmin → next app start opens Login
Run these on a real phone too

Frequently Asked Questions

Can I send "Your order was shipped" when the app is closed?
Not with local notifications — the app is not running. Use Firebase Cloud Messaging (FCM): save each device token in a table, and when the admin changes the status, PHP sends a push to that token with FCM's HTTP v1 API.
Can the user place an order while offline?
No. Reading (home, products, orders) works from the cache, but every POST needs the server, because only the server can check stock and prices. The app shows "No internet connection" with a Retry button.
← E-commerce App Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicRelease App & Host the API Your progress: 26 / 28 (93%) · Lesson 27 / 28 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free