Notifications, Errors और Offline
Order notification
Step 1: channel और permission
Android 8 से हर notification एक channel का होता है जिसे user settings में बंद कर सकता है। हम "Order updates" channel KartApp.onCreate() में बनाते हैं। Android 13 से app को POST_NOTIFICATIONS भी माँगनी पड़ती है; user मना करे तो app बिना notifications के normal चलती है।
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)
}
}// 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)
}
}<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>Step 2: tap पर order खुले
NavDeepLinkBuilder एक PendingIntent बनाता है जो MainActivity शुरू करके order id के साथ सीधे orderDetailFragment पर ले जाता है। यह सही back stack भी बनाता है (नीचे Home), इसलिए order से Back Home पर जाता है।
// 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(/* … */))
}हर जगह error handling
हर screen वही तीन rules follow करती है, इसलिए errors कभी app crash नहीं करते और user को हमेशा पता होता है कि क्या करना है:
| स्थिति | User को क्या दिखता है |
|---|---|
| Home load fail | cloud icon, "No internet connection…", Try again |
| Add करते समय stock ख़त्म | Snackbar "Only 3 left in stock." |
| गलत coupon | coupon field के नीचे red text |
| Token expire | login screen खुलती है |
| Scroll में अगला page fail | Snackbar; load हुए items रहते हैं |
Offline: banner + cache
Step 3: NetworkMonitor
ConnectivityManager.NetworkCallback connection आने या जाने पर एक StateFlow<Boolean> update करता है। MainActivity banner दिखाती है; OkHttp interceptor isOnline() use करता है।
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)
}
}// 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 करें:
// 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
Test checklist
- 1Order करें → notification "Order placed 🎉" → tap → order खुले
- 2Notification permission deny करें → ordering फिर भी चले, बस notification नहीं
- 3Home खोलें, फिर airplane mode → banner दिखे, pull to refresh पर भी Home दिखे
- 4Offline में कभी न खोला product खोलें → Try again वाला error state
- 5Internet वापस on → banner गायब, Try again load करे
- 6phpMyAdmin में token row delete करें → अगली बार app Login खोले
