Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
📱 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
Notification tap करने पर सीधे वही order खुलता है

Step 1: channel और permission

Android 8 से हर notification एक channel का होता है जिसे user settings में बंद कर सकता है। हम "Order updates" channel KartApp.onCreate() में बनाते हैं। Android 13 से app को POST_NOTIFICATIONS भी माँगनी पड़ती है; user मना करे तो app बिना notifications के normal चलती है।

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 (छोटा 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 एक PendingIntent बनाता है जो MainActivity शुरू करके order id के साथ सीधे orderDetailFragment पर ले जाता है। यह सही back stack भी बनाता है (नीचे Home), इसलिए order से Back Home पर जाता है।

Kotlin – CheckoutFragment.kt (जहाँ call होता है)
// 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()NavHostdeep link पढ़ता हैorderDetailFragmentorderId = 1
Notification से order screen तक

हर जगह error handling

हर screen वही तीन rules follow करती है, इसलिए errors कभी app crash नहीं करते और user को हमेशा पता होता है कि क्या करना है:

safeCallहर API call Result लौटाती है; कोई exception बाहर नहीं जाताUiState.Errorscreen load न हो तो "Try again" वाला full-screen errorSnackbarछोटे actions (add to cart, wishlist) API message दिखाते हैंField errorsvalidation TextInputLayout के नीचे401 handlingexpired token → अपने आप login पर
Friendly errors की पाँच layers
स्थितिUser को क्या दिखता है
Home load failcloud icon, "No internet connection…", Try again
Add करते समय stock ख़त्मSnackbar "Only 3 left in stock."
गलत couponcoupon field के नीचे red text
Token expirelogin screen खुलती है
Scroll में अगला page failSnackbar; load हुए items रहते हैं

Offline: banner + cache

9:41▮▮ 85%☰KartEasy⋮You are offline. Showing saveddata.Festive Salecache सेMobilesFashionBooks⌚Smart Watch Pro₹1,899🎧Earbuds X2₹1,099🏠Home🗂️Categories♡Wishlist🛒Cart👤Account
Airplane mode: banner दिखता है और Home फिर भी आख़िरी data दिखाती है

Step 3: NetworkMonitor

ConnectivityManager.NetworkCallback connection आने या जाने पर एक StateFlow<Boolean> update करता है। MainActivity banner दिखाती है; OkHttp interceptor isOnline() use करता है।

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 में 10 MB disk cache है। Trick यह है कि online होने पर हमेशा network use करें (ताकि cart और prices कभी पुराने न हों) पर फिर भी copy save करें, और copy सिर्फ offline होने पर use करें:

Kotlin – ApiClient.kt (offline वाला हिस्सा)
// 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
  • PHP से ताज़ा जवाब
  • copy disk पर save

Offline

  • onlyIfCached + maxStale 7 days
  • disk से जवाब
  • कभी cache नहीं हुआ → Retry वाला error
सिर्फ GET requests — POST को हमेशा server चाहिए

Test checklist

  1. 1Order करें → notification "Order placed 🎉" → tap → order खुले
  2. 2Notification permission deny करें → ordering फिर भी चले, बस notification नहीं
  3. 3Home खोलें, फिर airplane mode → banner दिखे, pull to refresh पर भी Home दिखे
  4. 4Offline में कभी न खोला product खोलें → Try again वाला error state
  5. 5Internet वापस on → banner गायब, Try again load करे
  6. 6phpMyAdmin में token row delete करें → अगली बार app Login खोले
इन्हें असली phone पर भी चलाएँ

अक्सर पूछे जाने वाले प्रश्न (FAQ)

App बंद होने पर "Your order was shipped" भेज सकते हैं?
Local notifications से नहीं — app चल ही नहीं रही। Firebase Cloud Messaging (FCM) use करें: हर device token table में save करें, और admin status बदले तो PHP FCM की HTTP v1 API से उस token को push भेजे।
क्या user offline order कर सकता है?
नहीं। पढ़ना (home, products, orders) cache से चलता है, पर हर POST के लिए server चाहिए, क्योंकि stock और prices सिर्फ server check कर सकता है। App Retry button के साथ "No internet connection" दिखाती है।
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

अगला topicApp Release और API Hosting आपकी प्रगति: 26 / 28 (93%) · Lesson 27 / 28 आगे बढ़ें →

यह lesson कैसा लगा?

आपकी राय से हम content और बेहतर बनाते हैं।

अपना review और सुझाव लिखें (वैकल्पिक)

Review approve होने के बाद आपका नाम और शहर इसके साथ public दिख सकते हैं। Email या phone न लिखें।

▶ CodingEasily on YouTube

Video से सीखें – आसान Hindi में

चुनी हुई video: RDBMS भाग 3 – Class XI Commerce IT

▶ Channel subscribe करें