Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
📱 Android App (Kotlin + XML)  ·  Lesson 17

Retrofit, Token और Session

Network layer एक picture में

ShopRepositorylogin(), cart(), placeOrder() … Result<T> लौटाता है; cart badge count रखता हैsafeCall { }exceptions और success:false को पढ़ने लायक message वाले Result.Error में बदलता हैApiService (Retrofit)हर endpoint के लिए एक suspend Kotlin functionOkHttp + interceptorsBearer token, offline cache, logging, timeoutsGsonJSON ⇄ Models.kt की data classes
Function call से HTTP request तक और वापस

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?) होने चाहिए।

Text – mapping
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: Boolean
Kotlin – data/model/Models.kt
package 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।

Kotlin – data/api/ApiService.kt
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 करते हैं:

authAuthorization: Bearer token जोड़ेंofflineonline: FORCE_NETWORK · offline: cacheloggingdebug builds में printnetworkcacheResponses GET को storable mark करता है
हर request के लिए OkHttp chain
Kotlin – data/api/ApiClient.kt
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)
    }
}
📌 cachedToken क्यों?

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." दिखता है।

Kotlin – data/api/Result.kt
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 बंदIOExceptionNo internet connection. Check your network and try again.
API ने 4xx / 5xx दियाHttpExceptionAPI का अपना message, जैसे "Please login to continue."
API ने 200 के साथ success:false दिया—res.message
खराब JSON / bugExceptione.message
Kotlin – Result use करना
// 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 करते हैं।

Kotlin – data/session/SessionManager.kt
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 करता है
नई apps के लिए Google DataStore recommend करता है

Step 6: ShopRepository

API से बात करने वाली इकलौती जगह। ViewModels कभी Retrofit नहीं देखते। Repository cartCount भी रखता है: हर cart call उसे update करती है, और MainActivity उसे Cart tab पर badge की तरह दिखाती है।

Kotlin – data/repo/ShopRepository.kt
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 खोलें:

Logcat
D/okhttp.OkHttpClient: --> POST http://10.0.2.2:8000/api/cart/add.php D/okhttp.OkHttpClient: Content-Type: application/json; charset=UTF-8 D/okhttp.OkHttpClient: Authorization: Bearer c4a45ae77d0b… D/okhttp.OkHttpClient: {"product_id":7,"qty":1} D/okhttp.OkHttpClient: --> END POST (26-byte body) D/okhttp.OkHttpClient: <-- 200 OK http://10.0.2.2:8000/api/cart/add.php (84ms) D/okhttp.OkHttpClient: {"success":true,"message":"Added to cart.","data":{"items":[…],"item_count":1,…}}
  • "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 क्यों है?
हर endpoint success, message और data लौटाता है, पर data हर बार अलग है (Cart, List, User…)। ApiResponse या ApiResponse> Gson को बताता है कि data के लिए कौन-सी class बनानी है।
क्या token के लिए DataStore safe है?
यह private app storage है जिसे दूसरी apps नहीं पढ़ सकतीं। Banking-level security के लिए value को Android Keystore से encrypt कर सकते हैं, पर shop app के लिए private storage + server-side expiry आम तरीका है।
App को मिलने वाला JSON कहाँ देखूँ?
Android Studio → Logcat, "okhttp" से filter करें। Debug builds में HttpLoggingInterceptor हर request, header और body print करता है।
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

अगला topicHome: Banners, Categories, Products आपकी प्रगति: 17 / 28 (61%) · Lesson 18 / 28 आगे बढ़ें →

यह lesson कैसा लगा?

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

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

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

▶ CodingEasily on YouTube

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

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

▶ Channel subscribe करें