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

Cart Screen

The screen

9:41▮▮ 85%☰Cart⋮🚚 Add ₹150 more for FREE delivery━━━━━━━━━━━━━━━━━━━━━━━━──────📘Learn Kotlin in 30 Days₹349− 1 +♡ Save for later🗑 RemovePrice (1 item)₹499Delivery charges₹40Total amount₹389Checkout🏠Home🗂️Categories♡Wishlist🛒Cart👤Account9:41▮▮ 85%☰Cart⋮🎉 You get FREE delivery on thisorder⌚Smart Watch Pro₹1,899− 1 +📘Learn Kotlin in 30 Days₹349 × 2− 1 +Total amount₹2,597You save ₹3,400 on this order🏠Home🗂️Categories♡Wishlist🛒Cart👤AccountLearn Kotlin removed UNDO
Below ₹499 a progress bar invites to add more; swipe removes an item with Undo

Step 1: layouts

The cart list sits inside a NestedScrollView together with the free-delivery card and the price summary, with nestedScrollingEnabled="false" so everything scrolls as one page. The total and the Checkout button are fixed at the bottom.

XML – layout/fragment_cart.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.core.widget.NestedScrollView
        android:id="@+id/content"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/bottom_bar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingTop="8dp">

            <!-- Free delivery progress -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:layout_marginEnd="12dp"
                android:background="@drawable/bg_offer_card"
                android:orientation="vertical"
                android:padding="12dp">

                <TextView
                    android:id="@+id/free_delivery"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:drawablePadding="8dp"
                    android:gravity="center_vertical"
                    android:textColor="@color/text_primary"
                    android:textStyle="bold"
                    app:drawableStartCompat="@drawable/ic_truck"
                    app:drawableTint="@color/brand"
                    tools:text="Add ₹120 more for FREE delivery" />

                <com.google.android.material.progressindicator.LinearProgressIndicator
                    android:id="@+id/free_delivery_bar"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:max="100"
                    app:trackCornerRadius="4dp"
                    app:trackThickness="6dp" />
            </LinearLayout>

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/list"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                android:nestedScrollingEnabled="false"
                tools:itemCount="2"
                tools:listitem="@layout/item_cart" />

            <include
                android:id="@+id/summary"
                layout="@layout/layout_price_summary" />
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>

    <com.google.android.material.progressindicator.LinearProgressIndicator
        android:id="@+id/updating"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:visibility="invisible"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <LinearLayout
        android:id="@+id/bottom_bar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@drawable/bg_bottom_bar"
        android:elevation="8dp"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="12dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical">

            <TextView
                android:id="@+id/total"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/text_primary"
                android:textSize="20sp"
                android:textStyle="bold"
                tools:text="₹2,337" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/total_amount"
                android:textColor="@color/text_secondary"
                android:textSize="12sp" />
        </LinearLayout>

        <com.google.android.material.button.MaterialButton
            android:id="@+id/checkout"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:backgroundTint="@color/accent"
            android:paddingStart="32dp"
            android:paddingEnd="32dp"
            android:text="@string/checkout"
            android:textColor="@android:color/white" />
    </LinearLayout>

    <include
        android:id="@+id/state"
        layout="@layout/layout_state" />
</androidx.constraintlayout.widget.ConstraintLayout>
XML – layout/item_cart.xml
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="12dp"
    android:layout_marginTop="6dp"
    android:layout_marginEnd="12dp"
    android:layout_marginBottom="6dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="12dp">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <com.google.android.material.imageview.ShapeableImageView
                android:id="@+id/image"
                android:layout_width="88dp"
                android:layout_height="88dp"
                android:contentDescription="@string/product_image"
                android:scaleType="centerCrop"
                app:shapeAppearanceOverlay="@style/ShapeAppearance.KartEasy.Rounded" />

            <LinearLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:layout_weight="1"
                android:orientation="vertical">

                <TextView
                    android:id="@+id/name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:ellipsize="end"
                    android:maxLines="2"
                    android:textColor="@color/text_primary"
                    tools:text="Smart Watch Pro" />

                <LinearLayout
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="4dp"
                    android:baselineAligned="true"
                    android:orientation="horizontal">

                    <TextView
                        android:id="@+id/price"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:textAppearance="@style/TextAppearance.KartEasy.Price"
                        tools:text="₹1,899" />

                    <TextView
                        android:id="@+id/mrp"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginStart="8dp"
                        android:textColor="@color/text_secondary"
                        android:textSize="12sp"
                        tools:text="₹4,999" />
                </LinearLayout>

                <TextView
                    android:id="@+id/warning"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="2dp"
                    android:textColor="@color/sale"
                    android:textSize="12sp"
                    android:textStyle="bold"
                    android:visibility="gone"
                    tools:text="Out of stock" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:gravity="center_vertical"
                    android:orientation="horizontal">

                    <com.google.android.material.button.MaterialButton
                        android:id="@+id/minus"
                        style="@style/Widget.KartEasy.QtyButton"
                        android:layout_width="36dp"
                        android:layout_height="36dp"
                        android:contentDescription="@string/decrease"
                        android:text="−" />

                    <TextView
                        android:id="@+id/qty"
                        android:layout_width="36dp"
                        android:layout_height="wrap_content"
                        android:gravity="center"
                        android:textColor="@color/text_primary"
                        android:textStyle="bold"
                        tools:text="2" />

                    <com.google.android.material.button.MaterialButton
                        android:id="@+id/plus"
                        style="@style/Widget.KartEasy.QtyButton"
                        android:layout_width="36dp"
                        android:layout_height="36dp"
                        android:contentDescription="@string/increase"
                        android:text="+" />

                    <TextView
                        android:id="@+id/line_total"
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:gravity="end"
                        android:textColor="@color/text_secondary"
                        android:textSize="13sp"
                        tools:text="Total: ₹3,798" />
                </LinearLayout>
            </LinearLayout>
        </LinearLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginTop="10dp"
            android:background="@color/divider" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">

            <com.google.android.material.button.MaterialButton
                android:id="@+id/save"
                style="@style/Widget.Material3.Button.TextButton.Icon"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/save_for_later"
                app:icon="@drawable/ic_heart_outline" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/remove"
                style="@style/Widget.Material3.Button.TextButton.Icon"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="@string/remove"
                android:textColor="@color/sale"
                app:icon="@drawable/ic_delete"
                app:iconTint="@color/sale" />
        </LinearLayout>
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>

Step 2: the shared price summary

Cart and Checkout show the same price box. It is one layout, included in both screens, and one extension function LayoutPriceSummaryBinding.bind(cart) fills it — plurals for "Price (3 items)" and the coupon row only when a coupon is applied.

XML – layout/layout_price_summary.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Price details card. Filled by PriceSummary.kt: binding.summary.bind(cart) -->
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="12dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <TextView
            style="@style/KartEasy.CardTitle"
            android:text="@string/price_details" />

        <LinearLayout style="@style/KartEasy.SummaryRow">
            <TextView
                android:id="@+id/items_label"
                style="@style/KartEasy.SummaryLabel"
                tools:text="Price (3 items)" />
            <TextView
                android:id="@+id/mrp_value"
                style="@style/KartEasy.SummaryValue"
                tools:text="₹5,997" />
        </LinearLayout>

        <LinearLayout style="@style/KartEasy.SummaryRow">
            <TextView
                style="@style/KartEasy.SummaryLabel"
                android:text="@string/product_discount" />
            <TextView
                android:id="@+id/product_discount_value"
                style="@style/KartEasy.SummaryValue"
                android:textColor="@color/success"
                tools:text="− ₹3,400" />
        </LinearLayout>

        <LinearLayout
            android:id="@+id/coupon_row"
            style="@style/KartEasy.SummaryRow"
            android:visibility="gone">
            <TextView
                android:id="@+id/coupon_label"
                style="@style/KartEasy.SummaryLabel"
                tools:text="Coupon (SAVE10)" />
            <TextView
                android:id="@+id/coupon_value"
                style="@style/KartEasy.SummaryValue"
                android:textColor="@color/success"
                tools:text="− ₹259.70" />
        </LinearLayout>

        <LinearLayout style="@style/KartEasy.SummaryRow">
            <TextView
                style="@style/KartEasy.SummaryLabel"
                android:text="@string/delivery_charges" />
            <TextView
                android:id="@+id/delivery_value"
                style="@style/KartEasy.SummaryValue"
                tools:text="FREE" />
        </LinearLayout>

        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginTop="8dp"
            android:layout_marginBottom="8dp"
            android:background="@color/divider" />

        <LinearLayout style="@style/KartEasy.SummaryRow">
            <TextView
                style="@style/KartEasy.SummaryLabel"
                android:text="@string/total_amount"
                android:textColor="@color/text_primary"
                android:textStyle="bold" />
            <TextView
                android:id="@+id/total_value"
                style="@style/KartEasy.SummaryValue"
                android:textStyle="bold"
                tools:text="₹2,337.30" />
        </LinearLayout>

        <TextView
            android:id="@+id/savings"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:textColor="@color/success"
            android:textStyle="bold"
            tools:text="You save ₹3,659.70 on this order" />
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>
Kotlin – ui/cart/PriceSummary.kt
package com.codingeasily.karteasy.ui.cart

import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.model.Cart
import com.codingeasily.karteasy.databinding.LayoutPriceSummaryBinding
import com.codingeasily.karteasy.util.Format
import com.codingeasily.karteasy.util.visibleIf

/** Fills layout_price_summary.xml. Used by both Cart and Checkout. */
fun LayoutPriceSummaryBinding.bind(cart: Cart) {
    val ctx = root.context
    itemsLabel.text = ctx.resources.getQuantityString(R.plurals.price_items, cart.itemCount, cart.itemCount)
    mrpValue.text = Format.price(cart.mrpTotal)
    productDiscountValue.text = ctx.getString(R.string.minus_amount, Format.price(cart.mrpTotal - cart.subtotal))
    couponRow.visibleIf(cart.discount > 0)
    couponLabel.text = ctx.getString(R.string.coupon_label, cart.couponCode ?: "")
    couponValue.text = ctx.getString(R.string.minus_amount, Format.price(cart.discount))
    deliveryValue.text = if (cart.deliveryFee > 0) Format.price(cart.deliveryFee) else ctx.getString(R.string.free)
    totalValue.text = Format.price(cart.total)
    savings.visibleIf(cart.youSave > 0)
    savings.text = ctx.getString(R.string.you_save, Format.price(cart.youSave))
}

Step 3: CartAdapter

The minus button is disabled at 1 (use Remove instead), the plus button at the stock or 10. Unavailable items are faded to 60% and show a red warning.

Kotlin – ui/cart/CartAdapter.kt
package com.codingeasily.karteasy.ui.cart

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.model.CartItem
import com.codingeasily.karteasy.databinding.ItemCartBinding
import com.codingeasily.karteasy.util.Format
import com.codingeasily.karteasy.util.load
import com.codingeasily.karteasy.util.strike
import com.codingeasily.karteasy.util.visibleIf

class CartAdapter(
    private val onQty: (CartItem, Int) -> Unit,
    private val onRemove: (CartItem) -> Unit,
    private val onSave: (CartItem) -> Unit,
    private val onOpen: (CartItem) -> Unit
) : ListAdapter<CartItem, CartAdapter.Holder>(DIFF) {

    inner class Holder(val b: ItemCartBinding) : RecyclerView.ViewHolder(b.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        Holder(ItemCartBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val item = getItem(position)
        val b = holder.b
        val ctx = b.root.context
        b.image.load(item.image)
        b.name.text = item.name
        b.price.text = Format.price(item.price)
        b.mrp.strike(Format.price(item.mrp))
        b.mrp.visibleIf(item.mrp > item.price)
        b.qty.text = item.qty.toString()
        b.lineTotal.text = ctx.getString(R.string.line_total, Format.price(item.lineTotal))
        b.minus.isEnabled = item.qty > 1
        b.plus.isEnabled = item.qty < minOf(10, item.stock)
        b.warning.visibleIf(!item.available)
        b.warning.text = if (item.stock <= 0) ctx.getString(R.string.out_of_stock)
        else ctx.getString(R.string.only_left, item.stock)
        b.root.alpha = if (item.available) 1f else 0.6f

        b.minus.setOnClickListener { onQty(item, -1) }
        b.plus.setOnClickListener { onQty(item, +1) }
        b.remove.setOnClickListener { onRemove(item) }
        b.save.setOnClickListener { onSave(item) }
        b.image.setOnClickListener { onOpen(item) }
        b.name.setOnClickListener { onOpen(item) }
    }

    companion object {
        val DIFF = object : DiffUtil.ItemCallback<CartItem>() {
            override fun areItemsTheSame(a: CartItem, b: CartItem) = a.productId == b.productId
            override fun areContentsTheSame(a: CartItem, b: CartItem) = a == b
        }
    }
}

Step 4: CartViewModel

Every action (− / +, remove, undo, move to wishlist) goes through update(): it ignores taps while a request is running (busy) and replaces the whole cart with the server's answer. The screen can never show totals that the server did not calculate.

Kotlin – ui/cart/CartViewModel.kt
package com.codingeasily.karteasy.ui.cart

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.codingeasily.karteasy.data.api.Result
import com.codingeasily.karteasy.data.api.UiState
import com.codingeasily.karteasy.data.model.Cart
import com.codingeasily.karteasy.data.model.CartItem
import com.codingeasily.karteasy.util.kartApp
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class CartViewModel(app: Application) : AndroidViewModel(app) {

    private val repo = app.kartApp.repository
    private val _state = MutableStateFlow<UiState<Cart>>(UiState.Loading)
    val state: StateFlow<UiState<Cart>> = _state

    private val _busy = MutableStateFlow(false)
    val busy: StateFlow<Boolean> = _busy

    private val _messages = MutableSharedFlow<String>()
    val messages: SharedFlow<String> = _messages

    fun load() {
        viewModelScope.launch {
            if (_state.value !is UiState.Success) _state.value = UiState.Loading
            _state.value = when (val r = repo.cart()) {
                is Result.Success -> UiState.Success(r.data)
                is Result.Error -> UiState.Error(r.message)
            }
        }
    }

    fun changeQty(item: CartItem, delta: Int) = update { repo.updateCart(item.productId, item.qty + delta) }

    fun remove(item: CartItem) = update { repo.removeFromCart(item.productId) }

    /** Undo for the "Item removed" snackbar. */
    fun restore(item: CartItem) = update { repo.addToCart(item.productId, item.qty) }

    fun moveToWishlist(item: CartItem) {
        viewModelScope.launch {
            val r = repo.toggleWishlist(item.productId)
            if (r is Result.Success && !r.data.inWishlist) repo.toggleWishlist(item.productId) // was already there
            update { repo.removeFromCart(item.productId) }
            _messages.emit("Moved to wishlist.")
        }
    }

    /** Every cart call returns the full new cart, so the screen simply shows that. */
    private fun update(call: suspend () -> Result<Cart>) {
        if (_busy.value) return
        _busy.value = true
        viewModelScope.launch {
            when (val r = call()) {
                is Result.Success -> _state.value = UiState.Success(r.data)
                is Result.Error -> _messages.emit(r.message)
            }
            _busy.value = false
        }
    }
}
tap +busy = truePOST cart/update.phpqty + 1server cartnew items + totalsstate = Successlist, summary, total redraw
One loop for every cart change

Step 5: CartFragment

The fragment shows the empty state with "Start shopping" when there are no items, otherwise the list, the summary, the bottom total and the free-delivery card.

Kotlin – ui/cart/CartFragment.kt
package com.codingeasily.karteasy.ui.cart

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.api.UiState
import com.codingeasily.karteasy.data.model.CartItem
import com.codingeasily.karteasy.databinding.FragmentCartBinding
import com.codingeasily.karteasy.ui.product.ProductDetailFragment
import com.codingeasily.karteasy.util.Format
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.done
import com.codingeasily.karteasy.util.empty
import com.codingeasily.karteasy.util.error
import com.codingeasily.karteasy.util.hide
import com.codingeasily.karteasy.util.loading
import com.codingeasily.karteasy.util.show
import com.codingeasily.karteasy.util.snack
import com.google.android.material.snackbar.Snackbar

class CartFragment : Fragment() {

    companion object {
        /** Must match FREE_DELIVERY_ABOVE in the PHP config. */
        const val FREE_DELIVERY_ABOVE = 499.0
    }

    private var _binding: FragmentCartBinding? = null
    private val binding get() = _binding!!
    private val vm: CartViewModel by viewModels()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        _binding = FragmentCartBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val adapter = CartAdapter(
            onQty = { item, d -> vm.changeQty(item, d) },
            onRemove = ::removeWithUndo,
            onSave = { vm.moveToWishlist(it) },
            onOpen = { findNavController().navigate(R.id.productDetailFragment, bundleOf(ProductDetailFragment.ARG_PRODUCT_ID to it.productId)) }
        )
        binding.list.layoutManager = LinearLayoutManager(requireContext())
        binding.list.adapter = adapter

        // Swipe left or right to remove an item.
        ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
            override fun onMove(rv: RecyclerView, vh: RecyclerView.ViewHolder, t: RecyclerView.ViewHolder) = false
            override fun onSwiped(vh: RecyclerView.ViewHolder, direction: Int) {
                val pos = vh.bindingAdapterPosition
                adapter.notifyItemChanged(pos)          // put the row back; the new cart list removes it
                removeWithUndo(adapter.currentList[pos])
            }
        }).attachToRecyclerView(binding.list)

        binding.checkout.setOnClickListener { findNavController().navigate(R.id.action_cart_to_checkout) }

        collectWhenStarted(vm.state) { state ->
            when (state) {
                is UiState.Loading -> { binding.content.hide(); binding.bottomBar.hide(); binding.state.loading() }
                is UiState.Error -> { binding.content.hide(); binding.bottomBar.hide(); binding.state.error(state.message) { vm.load() } }
                is UiState.Success -> {
                    val cart = state.data
                    adapter.submitList(cart.items)
                    if (cart.items.isEmpty()) {
                        binding.content.hide(); binding.bottomBar.hide()
                        binding.state.empty(R.drawable.ic_cart_empty, R.string.cart_empty, R.string.start_shopping) {
                            findNavController().navigate(R.id.homeFragment)
                        }
                        return@collectWhenStarted
                    }
                    binding.state.done(); binding.content.show(); binding.bottomBar.show()
                    binding.summary.bind(cart)
                    binding.total.text = Format.price(cart.total)
                    binding.checkout.isEnabled = cart.itemCount > 0

                    // "Add ₹120 more for FREE delivery" + progress bar
                    val left = FREE_DELIVERY_ABOVE - cart.subtotal
                    if (left > 0) {
                        binding.freeDelivery.text = getString(R.string.free_delivery_left, Format.price(left))
                        binding.freeDeliveryBar.progress = ((cart.subtotal / FREE_DELIVERY_ABOVE) * 100).toInt()
                        binding.freeDeliveryBar.show()
                    } else {
                        binding.freeDelivery.setText(R.string.free_delivery_unlocked)
                        binding.freeDeliveryBar.hide()
                    }
                }
            }
        }
        collectWhenStarted(vm.busy) { binding.updating.visibility = if (it) View.VISIBLE else View.INVISIBLE }
        collectWhenStarted(vm.messages) { snack(it) }
    }

    override fun onResume() {
        super.onResume()
        vm.load()   // the cart may have changed on another screen
    }

    private fun removeWithUndo(item: CartItem) {
        vm.remove(item)
        Snackbar.make(binding.root, getString(R.string.removed_item, item.name), Snackbar.LENGTH_LONG)
            .setAction(R.string.undo) { vm.restore(item) }
            .setAnchorView(binding.bottomBar)
            .show()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Swipe to remove + Undo

ItemTouchHelper detects a left or right swipe. We immediately ask the API to remove the item and show a Snackbar with Undo; Undo simply adds the same product and quantity back. The Snackbar is anchored above the bottom bar so it does not cover the Checkout button.

UserCartFragmentAPIswipe row ←remove.php {product_id}Snackbar "… removed UNDO"taps UNDOadd.php {product_id, qty}
Undo = add it back

The cart badge

The number on the Cart tab is a Material BadgeDrawable. It follows repository.cartCount, which every cart call updates — so adding from Home, Detail or Wishlist changes the badge instantly.

Kotlin – MainActivity.kt (badge part)
// MainActivity.kt – the repository's cartCount drives the badge on the Cart tab
launch {
    app.repository.cartCount.collect { count ->
        val badge = binding.bottomNav.getOrCreateBadge(R.id.cartFragment)
        badge.isVisible = count > 0
        badge.number = count
        badge.backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.sale)
    }
}

Frequently Asked Questions

Why reload the cart in onResume()?
The cart can change on other screens (product detail, wishlist "move to cart"). Reloading when the tab becomes visible keeps it correct; the old list stays on screen while the new one loads, so there is no flicker.
Why is FREE_DELIVERY_ABOVE also in the app?
Only for the progress bar text ("Add ₹120 more…"). The real delivery fee always comes from the API. If you change the rule in config.php, change this constant too.
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

Next topicWishlist Screen Your progress: 21 / 28 (75%) · Lesson 22 / 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