Retrofit, Token और Session
Network layer एक picture में
Step 1: Models (JSON → Kotlin)
API का हर JSON object एक Kotlin data class बनता है। PHP API snake_case keys use करती है, Kotlin camelCase, इसलिए @SerializedName दोनों को जोड़ता है। JSON में जो fields null हो सकते हैं वे Kotlin में nullable (String?) होने चाहिए।
JSON from the API Kotlin (Models.kt)
--------------------------------- -----------------------------------------------
"product_id": 7 @SerializedName("product_id") val productId: Int
"name": "Smart Watch Pro" val name: String
"price": 1899 val price: Double (1899 → 1899.0)
"image": null val image: String? (nullable!)
"in_wishlist": true @SerializedName("in_wishlist") val inWishlist: Booleanpackage com.codingeasily.karteasy.data.model
import com.google.gson.annotations.SerializedName
/** Every API response has this shape: {"success":true,"message":"...","data":{...}} */
data class ApiResponse<T>(
val success: Boolean,
val message: String,
val data: T?
)
data class User(
val id: Int,
val name: String,
val email: String,
val phone: String?,
@SerializedName("order_count") val orderCount: Int = 0,
@SerializedName("wishlist_count") val wishlistCount: Int = 0,
@SerializedName("cart_count") val cartCount: Int = 0
)
data class AuthData(val token: String, val user: User)
data class Banner(
val id: Int,
val title: String,
val subtitle: String?,
val image: String,
@SerializedName("link_type") val linkType: String,
@SerializedName("link_id") val linkId: Int?
)
data class Category(
val id: Int,
val name: String,
val image: String?,
@SerializedName("product_count") val productCount: Int = 0
)
data class Product(
val id: Int,
@SerializedName("category_id") val categoryId: Int,
val name: String,
@SerializedName("short_desc") val shortDesc: String?,
val price: Double,
val mrp: Double,
@SerializedName("discount_pct") val discountPct: Int,
val stock: Int,
val image: String?,
val rating: Double,
@SerializedName("rating_count") val ratingCount: Int,
@SerializedName("in_wishlist") val inWishlist: Boolean = false
)
data class HomeData(
val banners: List<Banner>,
val categories: List<Category>,
val featured: List<Product>,
val deals: List<Product>,
@SerializedName("new_arrivals") val newArrivals: List<Product>
)
data class ProductPage(
val items: List<Product>,
val page: Int,
val total: Int,
@SerializedName("has_more") val hasMore: Boolean
)
data class Review(
val name: String,
val rating: Int,
val comment: String?,
val date: String
)
data class ReviewPage(
val items: List<Review>,
val breakdown: Map<String, Int>
)
data class ProductDetail(
val id: Int,
val name: String,
@SerializedName("short_desc") val shortDesc: String?,
val description: String?,
@SerializedName("category_name") val categoryName: String,
val price: Double,
val mrp: Double,
@SerializedName("discount_pct") val discountPct: Int,
val stock: Int,
val image: String?,
val images: List<String>,
val rating: Double,
@SerializedName("rating_count") val ratingCount: Int,
@SerializedName("in_wishlist") val inWishlist: Boolean,
@SerializedName("cart_qty") val cartQty: Int,
val reviews: List<Review>,
val similar: List<Product>
)
data class CartItem(
@SerializedName("product_id") val productId: Int,
val name: String,
val image: String?,
val price: Double,
val mrp: Double,
val qty: Int,
val stock: Int,
@SerializedName("line_total") val lineTotal: Double,
val available: Boolean
)
data class Cart(
val items: List<CartItem>,
@SerializedName("item_count") val itemCount: Int,
@SerializedName("mrp_total") val mrpTotal: Double,
val subtotal: Double,
@SerializedName("you_save") val youSave: Double,
val discount: Double,
@SerializedName("coupon_code") val couponCode: String?,
@SerializedName("coupon_message") val couponMessage: String?,
@SerializedName("delivery_fee") val deliveryFee: Double,
val total: Double
)
data class WishlistToggle(
@SerializedName("product_id") val productId: Int,
@SerializedName("in_wishlist") val inWishlist: Boolean
)
data class Address(
val id: Int = 0,
val name: String,
val phone: String,
val line1: String,
val line2: String?,
val city: String,
val state: String,
val pincode: String,
@SerializedName("is_default") val isDefault: Boolean = false
) {
fun fullText(): String = buildString {
append(line1)
if (!line2.isNullOrBlank()) append(", ").append(line2)
append(", ").append(city).append(", ").append(state).append(" - ").append(pincode)
}
}
data class Coupon(
val code: String,
val title: String,
val description: String,
@SerializedName("min_order") val minOrder: Double
)
data class PlacedOrder(
@SerializedName("order_id") val orderId: Int,
@SerializedName("order_no") val orderNo: String,
val total: Double
)
data class OrderSummary(
val id: Int,
@SerializedName("order_no") val orderNo: String,
val total: Double,
val status: String,
@SerializedName("item_count") val itemCount: Int,
@SerializedName("first_image") val firstImage: String?,
@SerializedName("created_at") val createdAt: String,
@SerializedName("can_cancel") val canCancel: Boolean
)
data class OrderItem(
@SerializedName("product_id") val productId: Int,
val name: String,
val image: String?,
val price: Double,
val qty: Int,
@SerializedName("line_total") val lineTotal: Double
)
data class TimelineStep(val status: String, val done: Boolean)
data class OrderDetail(
val id: Int,
@SerializedName("order_no") val orderNo: String,
val status: String,
@SerializedName("created_at") val createdAt: String,
@SerializedName("can_cancel") val canCancel: Boolean,
val items: List<OrderItem>,
@SerializedName("ship_name") val shipName: String,
@SerializedName("ship_phone") val shipPhone: String,
@SerializedName("ship_address") val shipAddress: String,
val subtotal: Double,
val discount: Double,
@SerializedName("delivery_fee") val deliveryFee: Double,
val total: Double,
@SerializedName("coupon_code") val couponCode: String?,
@SerializedName("payment_method") val paymentMethod: String,
val timeline: List<TimelineStep>
)
// ---------- request bodies (sent as JSON with @Body)
data class LoginRequest(val email: String, val password: String)
data class RegisterRequest(val name: String, val email: String, val phone: String, val password: String)
data class UpdateProfileRequest(
val name: String,
val phone: String,
@SerializedName("current_password") val currentPassword: String = "",
@SerializedName("new_password") val newPassword: String = ""
)
data class CartRequest(@SerializedName("product_id") val productId: Int, val qty: Int = 1)
data class ProductIdRequest(@SerializedName("product_id") val productId: Int)
data class IdRequest(val id: Int)
data class CouponRequest(val code: String)
data class PlaceOrderRequest(
@SerializedName("address_id") val addressId: Int,
val coupon: String,
@SerializedName("payment_method") val paymentMethod: String = "COD"
)
data class ReviewRequest(
@SerializedName("product_id") val productId: Int,
val rating: Int,
val comment: String
)
data class AddressRequest(
val id: Int,
val name: String,
val phone: String,
val line1: String,
val line2: String,
val city: String,
val state: String,
val pincode: String,
@SerializedName("is_default") val isDefault: Int
)Step 2: ApiService
Retrofit इसी interface से HTTP code लिख देता है। @Body objects JSON में जाते हैं; @Query values ?key=value बनती हैं, और null values अपने आप छूट जाती हैं — optional filters के लिए perfect।
package com.codingeasily.karteasy.data.api
import com.codingeasily.karteasy.data.model.*
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query
/**
* Every backend endpoint in one interface. Retrofit writes the code for us.
* The Authorization header is added automatically by AuthInterceptor.
*/
interface ApiService {
// ---------- auth
@POST("api/auth/register.php")
suspend fun register(@Body body: RegisterRequest): ApiResponse<AuthData>
@POST("api/auth/login.php")
suspend fun login(@Body body: LoginRequest): ApiResponse<AuthData>
@POST("api/auth/logout.php")
suspend fun logout(): ApiResponse<Any>
@GET("api/auth/me.php")
suspend fun me(): ApiResponse<User>
@POST("api/auth/update.php")
suspend fun updateProfile(@Body body: UpdateProfileRequest): ApiResponse<User>
// ---------- catalog
@GET("api/home.php")
suspend fun home(): ApiResponse<HomeData>
@GET("api/categories.php")
suspend fun categories(): ApiResponse<List<Category>>
@GET("api/products.php")
suspend fun products(
@Query("q") query: String? = null,
@Query("category_id") categoryId: Int? = null,
@Query("min_price") minPrice: Int? = null,
@Query("max_price") maxPrice: Int? = null,
@Query("sort") sort: String = "newest",
@Query("page") page: Int = 1,
@Query("limit") limit: Int = 20
): ApiResponse<ProductPage>
@GET("api/product.php")
suspend fun product(@Query("id") id: Int): ApiResponse<ProductDetail>
@GET("api/reviews/list.php")
suspend fun reviews(@Query("product_id") productId: Int, @Query("page") page: Int = 1): ApiResponse<ReviewPage>
@POST("api/reviews/add.php")
suspend fun addReview(@Body body: ReviewRequest): ApiResponse<Any>
// ---------- cart
@GET("api/cart/list.php")
suspend fun cart(@Query("coupon") coupon: String? = null): ApiResponse<Cart>
@POST("api/cart/add.php")
suspend fun addToCart(@Body body: CartRequest): ApiResponse<Cart>
@POST("api/cart/update.php")
suspend fun updateCart(@Body body: CartRequest): ApiResponse<Cart>
@POST("api/cart/remove.php")
suspend fun removeFromCart(@Body body: ProductIdRequest): ApiResponse<Cart>
// ---------- wishlist
@GET("api/wishlist/list.php")
suspend fun wishlist(): ApiResponse<List<Product>>
@POST("api/wishlist/toggle.php")
suspend fun toggleWishlist(@Body body: ProductIdRequest): ApiResponse<WishlistToggle>
// ---------- addresses
@GET("api/address/list.php")
suspend fun addresses(): ApiResponse<List<Address>>
@POST("api/address/save.php")
suspend fun saveAddress(@Body body: AddressRequest): ApiResponse<Address>
@POST("api/address/delete.php")
suspend fun deleteAddress(@Body body: IdRequest): ApiResponse<Any>
// ---------- coupons
@GET("api/coupons/list.php")
suspend fun coupons(): ApiResponse<List<Coupon>>
@POST("api/coupons/apply.php")
suspend fun applyCoupon(@Body body: CouponRequest): ApiResponse<Cart>
// ---------- orders
@POST("api/orders/place.php")
suspend fun placeOrder(@Body body: PlaceOrderRequest): ApiResponse<PlacedOrder>
@GET("api/orders/list.php")
suspend fun orders(): ApiResponse<List<OrderSummary>>
@GET("api/orders/detail.php")
suspend fun orderDetail(@Query("id") id: Int): ApiResponse<OrderDetail>
@POST("api/orders/cancel.php")
suspend fun cancelOrder(@Body body: IdRequest): ApiResponse<Any>
}Step 3: ApiClient और interceptors
Interceptor वह code है जो हर request पर चलता है। हम तीन use करते हैं:
package com.codingeasily.karteasy.data.api
import android.content.Context
import com.codingeasily.karteasy.BuildConfig
import com.codingeasily.karteasy.data.session.SessionManager
import com.codingeasily.karteasy.util.NetworkMonitor
import okhttp3.Cache
import okhttp3.CacheControl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.util.concurrent.TimeUnit
/** Builds the single Retrofit instance used by the whole app. */
object ApiClient {
/** Kept so we can clear one user's cached data when they log out. */
var cache: Cache? = null
private set
fun create(context: Context, session: SessionManager, network: NetworkMonitor): ApiService {
// 1) Adds "Authorization: Bearer <token>" when the user is logged in.
val auth = Interceptor { chain ->
val token = session.cachedToken
val request = if (token.isNullOrEmpty()) chain.request()
else chain.request().newBuilder().header("Authorization", "Bearer $token").build()
chain.proceed(request)
}
// 2) Offline support.
// 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())
}
// 3) 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
}
val logging = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
}
val httpCache = Cache(File(context.cacheDir, "http"), 10L * 1024 * 1024) // 10 MB
cache = httpCache
val client = OkHttpClient.Builder()
.cache(httpCache)
.addInterceptor(auth)
.addInterceptor(offline)
.addNetworkInterceptor(cacheResponses)
.addInterceptor(logging)
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build()
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}Interceptors OkHttp की background threads पर चलते हैं और DataStore का इंतज़ार नहीं कर सकते। SessionManager load() और login के बाद token को @Volatile variable में रखता है, ताकि interceptor उसे तुरंत पढ़ सके।
Step 4: Result + safeCall
Network calls कई तरह से fail हो सकती हैं। safeCall सबको पकड़कर या तो Result.Success या दिखाने लायक message वाला Result.Error लौटाता है। HTTP errors (401, 409, 422 …) के लिए यह error body से हमारा JSON message पढ़ता है, इसलिए user को "HTTP 409" की जगह "Only 12 left in stock." दिखता है।
package com.codingeasily.karteasy.data.api
import com.codingeasily.karteasy.data.model.ApiResponse
import com.google.gson.Gson
import retrofit2.HttpException
import java.io.IOException
/** What every repository call returns: data, or a message we can show to the user. */
sealed class Result<out T> {
data class Success<T>(val data: T, val message: String) : Result<T>()
data class Error(val message: String, val code: Int = 0) : Result<Nothing>()
}
/** UI state for screens that load something. */
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
/**
* Runs one API call and turns every possible failure into Result.Error:
* - no internet / timeout -> IOException
* - HTTP 4xx/5xx from our PHP API -> HttpException (the body still has our JSON "message")
* - success=false in the JSON -> handled below
*/
suspend fun <T> safeCall(block: suspend () -> ApiResponse<T>): Result<T> {
return try {
val res = block()
val data = res.data
if (res.success && data != null) Result.Success(data, res.message)
else if (res.success) {
// Endpoints like logout return data = null on success.
@Suppress("UNCHECKED_CAST")
Result.Success(Unit as T, res.message)
} else Result.Error(res.message)
} catch (e: HttpException) {
val body = e.response()?.errorBody()?.string()
val msg = try {
Gson().fromJson(body, ApiResponse::class.java)?.message
} catch (ignored: Exception) {
null
}
Result.Error(msg ?: "Server error (${e.code()}). Please try again.", e.code())
} catch (e: IOException) {
Result.Error("No internet connection. Check your network and try again.")
} catch (e: Exception) {
Result.Error(e.message ?: "Something went wrong.")
}
}| क्या गलत हुआ | Exception | दिखने वाला message |
|---|---|---|
| Internet नहीं, timeout, server बंद | IOException | No internet connection. Check your network and try again. |
| API ने 4xx / 5xx दिया | HttpException | API का अपना message, जैसे "Please login to continue." |
| API ने 200 के साथ success:false दिया | — | res.message |
| खराब JSON / bug | Exception | e.message |
// In a ViewModel: one 'when' handles every outcome
viewModelScope.launch {
when (val r = repo.addToCart(productId)) {
is Result.Success -> _messages.emit(r.message) // "Added to cart."
is Result.Error -> _messages.emit(r.message) // "Only 12 left in stock." / "No internet…"
}
}Step 5: SessionManager (DataStore)
DataStore SharedPreferences का modern replacement है: asynchronous (UI thread कभी block नहीं), coroutines और Flow के साथ चलता है। हम token, user का name, email, phone, onboarding flag और theme store करते हैं।
package com.codingeasily.karteasy.data.session
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.codingeasily.karteasy.data.model.User
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
private val Context.dataStore by preferencesDataStore(name = "karteasy_session")
/**
* Stores the login token, basic user info and app settings with DataStore.
* cachedToken keeps the token in memory so the OkHttp interceptor can read it
* without blocking.
*/
class SessionManager(private val context: Context) {
private object Keys {
val TOKEN = stringPreferencesKey("token")
val NAME = stringPreferencesKey("name")
val EMAIL = stringPreferencesKey("email")
val PHONE = stringPreferencesKey("phone")
val ONBOARDING_DONE = booleanPreferencesKey("onboarding_done")
val DARK_MODE = stringPreferencesKey("dark_mode") // "system", "light", "dark"
}
@Volatile
var cachedToken: String? = null
private set
/** Call once at app start (from the splash screen). */
suspend fun load() {
cachedToken = context.dataStore.data.first()[Keys.TOKEN]
}
val isLoggedIn: Boolean get() = !cachedToken.isNullOrEmpty()
suspend fun isOnboardingDone(): Boolean = context.dataStore.data.first()[Keys.ONBOARDING_DONE] == true
suspend fun setOnboardingDone() {
context.dataStore.edit { it[Keys.ONBOARDING_DONE] = true }
}
suspend fun saveLogin(token: String, user: User) {
cachedToken = token
context.dataStore.edit {
it[Keys.TOKEN] = token
it[Keys.NAME] = user.name
it[Keys.EMAIL] = user.email
it[Keys.PHONE] = user.phone ?: ""
}
}
suspend fun updateUser(user: User) {
context.dataStore.edit {
it[Keys.NAME] = user.name
it[Keys.EMAIL] = user.email
it[Keys.PHONE] = user.phone ?: ""
}
}
/** Name + email for the drawer header. */
val userFlow: Flow<Pair<String, String>> = context.dataStore.data.map {
Pair(it[Keys.NAME] ?: "", it[Keys.EMAIL] ?: "")
}
suspend fun clearLogin() {
cachedToken = null
context.dataStore.edit {
it.remove(Keys.TOKEN)
it.remove(Keys.NAME)
it.remove(Keys.EMAIL)
it.remove(Keys.PHONE)
}
}
suspend fun darkMode(): String = context.dataStore.data.first()[Keys.DARK_MODE] ?: "system"
suspend fun setDarkMode(mode: String) {
context.dataStore.edit { it[Keys.DARK_MODE] = mode }
}
}SharedPreferences
- main thread पर synchronous reads
- commit()/apply() की उलझन
- default में change notifications नहीं
DataStore (यहाँ use)
- suspend functions + Flow
- किसी भी thread से safe
- userFlow drawer header को live update करता है
Step 6: ShopRepository
API से बात करने वाली इकलौती जगह। ViewModels कभी Retrofit नहीं देखते। Repository cartCount भी रखता है: हर cart call उसे update करती है, और MainActivity उसे Cart tab पर badge की तरह दिखाती है।
package com.codingeasily.karteasy.data.repo
import com.codingeasily.karteasy.data.api.ApiClient
import com.codingeasily.karteasy.data.api.ApiService
import com.codingeasily.karteasy.data.api.Result
import com.codingeasily.karteasy.data.api.safeCall
import com.codingeasily.karteasy.data.model.*
import com.codingeasily.karteasy.data.session.SessionManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
/**
* The only class that talks to the API. ViewModels call these functions.
* It also keeps the cart badge count in one place so every screen sees the same number.
*/
class ShopRepository(private val api: ApiService, private val session: SessionManager) {
private val _cartCount = MutableStateFlow(0)
val cartCount: StateFlow<Int> = _cartCount.asStateFlow()
private fun updateCount(result: Result<Cart>): Result<Cart> {
if (result is Result.Success) _cartCount.value = result.data.itemCount
return result
}
// ---------- auth
suspend fun login(email: String, password: String): Result<AuthData> {
val r = safeCall { api.login(LoginRequest(email.trim(), password)) }
if (r is Result.Success) session.saveLogin(r.data.token, r.data.user)
return r
}
suspend fun register(name: String, email: String, phone: String, password: String): Result<AuthData> {
val r = safeCall { api.register(RegisterRequest(name.trim(), email.trim(), phone.trim(), password)) }
if (r is Result.Success) session.saveLogin(r.data.token, r.data.user)
return r
}
suspend fun logout() {
safeCall { api.logout() } // even if this fails we log out locally
session.clearLogin()
_cartCount.value = 0
withContext(Dispatchers.IO) { ApiClient.cache?.evictAll() }
}
suspend fun me(): Result<User> {
val r = safeCall { api.me() }
if (r is Result.Success) {
_cartCount.value = r.data.cartCount
session.updateUser(r.data)
}
return r
}
suspend fun updateProfile(name: String, phone: String, currentPass: String, newPass: String): Result<User> {
val r = safeCall { api.updateProfile(UpdateProfileRequest(name.trim(), phone.trim(), currentPass, newPass)) }
if (r is Result.Success) session.updateUser(r.data)
return r
}
// ---------- catalog
suspend fun home() = safeCall { api.home() }
suspend fun categories() = safeCall { api.categories() }
suspend fun products(query: String?, categoryId: Int?, minPrice: Int?, maxPrice: Int?, sort: String, page: Int) =
safeCall { api.products(query, categoryId, minPrice, maxPrice, sort, page) }
suspend fun product(id: Int) = safeCall { api.product(id) }
suspend fun reviews(productId: Int) = safeCall { api.reviews(productId) }
suspend fun addReview(productId: Int, rating: Int, comment: String) =
safeCall { api.addReview(ReviewRequest(productId, rating, comment.trim())) }
// ---------- cart
suspend fun cart(coupon: String? = null) = updateCount(safeCall { api.cart(coupon) })
suspend fun addToCart(productId: Int, qty: Int = 1) = updateCount(safeCall { api.addToCart(CartRequest(productId, qty)) })
suspend fun updateCart(productId: Int, qty: Int) = updateCount(safeCall { api.updateCart(CartRequest(productId, qty)) })
suspend fun removeFromCart(productId: Int) = updateCount(safeCall { api.removeFromCart(ProductIdRequest(productId)) })
suspend fun applyCoupon(code: String) = updateCount(safeCall { api.applyCoupon(CouponRequest(code.trim())) })
suspend fun coupons() = safeCall { api.coupons() }
// ---------- wishlist
suspend fun wishlist() = safeCall { api.wishlist() }
suspend fun toggleWishlist(productId: Int) = safeCall { api.toggleWishlist(ProductIdRequest(productId)) }
// ---------- addresses
suspend fun addresses() = safeCall { api.addresses() }
suspend fun saveAddress(a: Address) = safeCall {
api.saveAddress(
AddressRequest(a.id, a.name.trim(), a.phone.trim(), a.line1.trim(), a.line2?.trim() ?: "",
a.city.trim(), a.state.trim(), a.pincode.trim(), if (a.isDefault) 1 else 0)
)
}
suspend fun deleteAddress(id: Int) = safeCall { api.deleteAddress(IdRequest(id)) }
// ---------- orders
suspend fun placeOrder(addressId: Int, coupon: String): Result<PlacedOrder> {
val r = safeCall { api.placeOrder(PlaceOrderRequest(addressId, coupon)) }
if (r is Result.Success) _cartCount.value = 0 // the server emptied the cart
return r
}
suspend fun orders() = safeCall { api.orders() }
suspend fun orderDetail(id: Int) = safeCall { api.orderDetail(id) }
suspend fun cancelOrder(id: Int) = safeCall { api.cancelOrder(IdRequest(id)) }
}Network calls debug करना
Debug app चलाएँ और okhttp filter के साथ Logcat खोलें:
- "CLEARTEXT communication not permitted" → IP
network_security_config.xmlमें नहीं है। - "failed to connect to /10.0.2.2" → PHP server नहीं चल रहा, या
0.0.0.0के साथ नहीं चला। - Gson "Expected BEGIN_OBJECT but was STRING" → PHP ने JSON से पहले error/HTML print किया; URL browser में खोलकर देखें।
- हमेशा 401 → token save नहीं हुआ, या hosting Authorization header हटा रही है (.htaccess rule देखें)।
अक्सर पूछे जाने वाले प्रश्न (FAQ)
ApiResponse में generic क्यों है?
- > Gson को बताता है कि data के लिए कौन-सी class बनानी है।
