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

Product Detail Screen

The screen

9:41▮▮ 85%←Electronics⋮🖼️swipe → 2 photos ━ •Smart Watch ProAMOLED, SpO2, 7-day battery4.5 ★ 128 ratings62% off ₹4,999 ₹1,899In stock · 🚚 Free delivery above₹499🛒 Add to cartBuy now9:41▮▮ 85%←Electronics⋮Quantity− 1 +Product details1.43 inch AMOLED screen, heart-rateand SpO2 tracking, 100+ sportsmodes…Ratings & reviews (128)★★★★★ Riya SharmaBattery easily lasts a …Similar products🔊Speaker Boom₹2,499🔋Power Bank₹1,499
Top of the detail screen, and further down (the buttons stay fixed at the bottom)

Step 1: fragment_product_detail.xml

A ConstraintLayout with three parts: the NestedScrollView (gallery, info card, description, reviews, similar products), the fixed bottom bar, and the shared state view. Each block has a white background on the grey screen, so the page reads as separate cards.

XML – layout/fragment_product_detail.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">

            <!-- Image gallery (swipe) + dots -->
            <androidx.viewpager2.widget.ViewPager2
                android:id="@+id/gallery"
                android:layout_width="match_parent"
                android:layout_height="320dp"
                android:background="@color/surface" />

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/gallery_dots"
                android:layout_width="match_parent"
                android:layout_height="20dp"
                android:background="@color/surface"
                app:tabBackground="@drawable/tab_dot"
                app:tabGravity="center"
                app:tabIndicatorHeight="0dp"
                app:tabMaxWidth="18dp"
                app:tabMinWidth="18dp"
                app:tabPaddingEnd="0dp"
                app:tabPaddingStart="0dp" />

            <!-- Name, rating, price -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/surface"
                android:orientation="vertical"
                android:padding="16dp">

                <TextView
                    android:id="@+id/name"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:textAppearance="?attr/textAppearanceTitleLarge"
                    android:textColor="@color/text_primary"
                    tools:text="Smart Watch Pro" />

                <TextView
                    android:id="@+id/short_desc"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="2dp"
                    android:textColor="@color/text_secondary"
                    tools:text="AMOLED, SpO2, 7-day battery" />

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

                    <TextView
                        android:id="@+id/rating_chip"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:background="@drawable/bg_rating"
                        android:paddingStart="8dp"
                        android:paddingTop="2dp"
                        android:paddingEnd="8dp"
                        android:paddingBottom="2dp"
                        android:textColor="@android:color/white"
                        android:textStyle="bold"
                        tools:text="4.5 ★" />

                    <TextView
                        android:id="@+id/rating_count"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginStart="8dp"
                        android:textColor="@color/text_secondary"
                        tools:text="128 ratings" />
                </LinearLayout>

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

                    <TextView
                        android:id="@+id/discount"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:layout_marginEnd="8dp"
                        android:textColor="@color/success"
                        android:textSize="20sp"
                        android:textStyle="bold"
                        tools:text="62% off" />

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

                    <TextView
                        android:id="@+id/price"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:textColor="@color/text_primary"
                        android:textSize="24sp"
                        android:textStyle="bold"
                        tools:text="₹1,899" />
                </LinearLayout>

                <TextView
                    android:id="@+id/stock"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:textStyle="bold"
                    tools:text="In stock" />

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:drawablePadding="8dp"
                    android:gravity="center_vertical"
                    android:text="@string/free_delivery_note"
                    android:textColor="@color/text_secondary"
                    android:textSize="13sp"
                    app:drawableStartCompat="@drawable/ic_truck"
                    app:drawableTint="@color/brand" />

                <!-- Quantity stepper -->
                <LinearLayout
                    android:id="@+id/qty_row"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="16dp"
                    android:gravity="center_vertical"
                    android:orientation="horizontal">

                    <TextView
                        android:layout_width="0dp"
                        android:layout_height="wrap_content"
                        android:layout_weight="1"
                        android:text="@string/quantity"
                        android:textColor="@color/text_primary"
                        android:textStyle="bold" />

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

                    <TextView
                        android:id="@+id/qty"
                        android:layout_width="44dp"
                        android:layout_height="wrap_content"
                        android:gravity="center"
                        android:textAppearance="?attr/textAppearanceTitleMedium"
                        android:textColor="@color/text_primary"
                        tools:text="1" />

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

                <TextView
                    android:id="@+id/in_cart"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="8dp"
                    android:textColor="@color/brand"
                    android:visibility="gone"
                    tools:text="1 already in your cart" />
            </LinearLayout>

            <!-- Description -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:background="@color/surface"
                android:orientation="vertical"
                android:padding="16dp">

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

                <TextView
                    android:id="@+id/description"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:ellipsize="end"
                    android:lineSpacingExtra="4dp"
                    android:maxLines="4"
                    android:textColor="@color/text_primary" />

                <TextView
                    android:id="@+id/description_toggle"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_marginTop="4dp"
                    android:background="?attr/selectableItemBackground"
                    android:paddingTop="4dp"
                    android:paddingBottom="4dp"
                    android:text="@string/read_more"
                    android:textColor="@color/brand"
                    android:textStyle="bold" />
            </LinearLayout>

            <!-- Reviews -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:background="@color/surface"
                android:orientation="vertical"
                android:padding="16dp">

                <TextView
                    android:id="@+id/reviews_title"
                    style="@style/KartEasy.CardTitle"
                    tools:text="Ratings &amp; reviews (128)" />

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/review_list"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:nestedScrollingEnabled="false"
                    tools:listitem="@layout/item_review" />

                <TextView
                    android:id="@+id/no_reviews"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:text="@string/no_reviews"
                    android:textColor="@color/text_secondary"
                    android:visibility="gone" />
            </LinearLayout>

            <!-- Similar products -->
            <TextView
                android:id="@+id/similar_title"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:paddingStart="16dp"
                android:paddingTop="12dp"
                android:paddingEnd="16dp"
                android:text="@string/similar_products"
                android:textAppearance="@style/TextAppearance.KartEasy.Section" />

            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/similar_list"
                style="@style/KartEasy.HorizontalRow"
                android:paddingBottom="16dp"
                tools:listitem="@layout/item_product" />
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>

    <!-- Fixed bottom bar: Add to cart / Buy now -->
    <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:orientation="horizontal"
        android:padding="12dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <com.google.android.material.button.MaterialButton
            android:id="@+id/add_to_cart"
            style="@style/Widget.KartEasy.Button.Outlined"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="6dp"
            android:layout_weight="1"
            android:text="@string/add_to_cart"
            app:icon="@drawable/ic_cart" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/buy_now"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="6dp"
            android:layout_weight="1"
            android:backgroundTint="@color/accent"
            android:text="@string/buy_now"
            android:textColor="@android:color/white" />
    </LinearLayout>

    <include
        android:id="@+id/state"
        layout="@layout/layout_state" />
</androidx.constraintlayout.widget.ConstraintLayout>

Step 2: gallery and review items

The gallery is another ViewPager2; the images use fitCenter so the whole product is visible. Reviews show a small indicator RatingBar.

XML – layout/item_gallery_image.xml
<?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/surface"
    android:contentDescription="@string/product_image"
    android:scaleType="fitCenter" />
Kotlin – ui/product/GalleryAdapter.kt
package com.codingeasily.karteasy.ui.product

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.karteasy.databinding.ItemGalleryImageBinding
import com.codingeasily.karteasy.util.load

class GalleryAdapter(private val images: List<String>) : RecyclerView.Adapter<GalleryAdapter.Holder>() {
    inner class Holder(val b: ItemGalleryImageBinding) : RecyclerView.ViewHolder(b.root)

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

    override fun getItemCount() = images.size

    override fun onBindViewHolder(holder: Holder, position: Int) = holder.b.image.load(images[position])
}
XML – layout/item_review.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:orientation="vertical"
    android:paddingTop="10dp"
    android:paddingBottom="10dp">

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

        <RatingBar
            android:id="@+id/rating_bar"
            style="?android:attr/ratingBarStyleSmall"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:isIndicator="true"
            android:numStars="5"
            android:progressTint="@color/rating" />

        <TextView
            android:id="@+id/name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_weight="1"
            android:textColor="@color/text_primary"
            android:textStyle="bold"
            tools:text="Riya Sharma" />

        <TextView
            android:id="@+id/date"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textColor="@color/text_secondary"
            android:textSize="12sp"
            tools:text="2026-10-19" />
    </LinearLayout>

    <TextView
        android:id="@+id/comment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="4dp"
        android:textColor="@color/text_primary"
        tools:text="Battery easily lasts a week." />
</LinearLayout>
Kotlin – ui/product/ReviewAdapter.kt
package com.codingeasily.karteasy.ui.product

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.karteasy.data.model.Review
import com.codingeasily.karteasy.databinding.ItemReviewBinding
import com.codingeasily.karteasy.util.visibleIf

class ReviewAdapter(private val reviews: List<Review>) : RecyclerView.Adapter<ReviewAdapter.Holder>() {
    inner class Holder(val b: ItemReviewBinding) : RecyclerView.ViewHolder(b.root)

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

    override fun getItemCount() = reviews.size

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val r = reviews[position]
        holder.b.name.text = r.name
        holder.b.ratingBar.rating = r.rating.toFloat()
        holder.b.date.text = r.date
        holder.b.comment.text = r.comment
        holder.b.comment.visibleIf(!r.comment.isNullOrBlank())
    }
}

Step 3: ProductDetailViewModel

The product id arrives through SavedStateHandle. Besides the product state, the ViewModel keeps the chosen quantity (limited by stock and 10), a busy flag that disables both buttons while adding, and one-time events: a message, or "go to cart" after Buy now.

Kotlin – ui/product/ProductDetailViewModel.kt
package com.codingeasily.karteasy.ui.product

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.codingeasily.karteasy.data.api.Result
import com.codingeasily.karteasy.data.api.UiState
import com.codingeasily.karteasy.data.model.ProductDetail
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 ProductDetailViewModel(app: Application, args: SavedStateHandle) : AndroidViewModel(app) {

    sealed class Event {
        data class Message(val text: String) : Event()
        object GoToCart : Event()
    }

    private val repo = app.kartApp.repository
    val productId: Int = args.get<Int>(ProductDetailFragment.ARG_PRODUCT_ID) ?: 0

    private val _state = MutableStateFlow<UiState<ProductDetail>>(UiState.Loading)
    val state: StateFlow<UiState<ProductDetail>> = _state

    /** Quantity chosen with the − / + buttons. */
    private val _qty = MutableStateFlow(1)
    val qty: StateFlow<Int> = _qty

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

    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events

    init { load() }

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

    fun changeQty(delta: Int) {
        val p = (_state.value as? UiState.Success)?.data ?: return
        _qty.value = (_qty.value + delta).coerceIn(1, minOf(10, maxOf(1, p.stock)))
    }

    /** buyNow = true opens the cart right after adding. */
    fun addToCart(buyNow: Boolean) {
        if (_busy.value) return
        _busy.value = true
        viewModelScope.launch {
            when (val r = repo.addToCart(productId, _qty.value)) {
                is Result.Success -> {
                    _events.emit(Event.Message(r.message))
                    if (buyNow) _events.emit(Event.GoToCart)
                    _qty.value = 1
                    load()   // refreshes "already in cart" count and stock
                }
                is Result.Error -> _events.emit(Event.Message(r.message))
            }
            _busy.value = false
        }
    }

    fun toggleWishlist() {
        viewModelScope.launch {
            when (val r = repo.toggleWishlist(productId)) {
                is Result.Success -> {
                    val cur = (_state.value as? UiState.Success)?.data
                    if (cur != null) _state.value = UiState.Success(cur.copy(inWishlist = r.data.inWishlist))
                    _events.emit(Event.Message(r.message))
                }
                is Result.Error -> _events.emit(Event.Message(r.message))
            }
        }
    }
}

Step 4: ProductDetailFragment

bind() fills every view. The gallery adapter is created only the first time (otherwise a refresh after "Add to cart" would jump back to photo 1). Stock text turns red when 5 or fewer are left.

Kotlin – ui/product/ProductDetailFragment.kt
package com.codingeasily.karteasy.ui.product

import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.MenuProvider
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.api.UiState
import com.codingeasily.karteasy.data.model.ProductDetail
import com.codingeasily.karteasy.databinding.FragmentProductDetailBinding
import com.codingeasily.karteasy.util.Format
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.done
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.codingeasily.karteasy.util.strike
import com.codingeasily.karteasy.util.visibleIf
import com.google.android.material.tabs.TabLayoutMediator

class ProductDetailFragment : Fragment() {

    companion object {
        const val ARG_PRODUCT_ID = "productId"
    }

    private var _binding: FragmentProductDetailBinding? = null
    private val binding get() = _binding!!
    private val vm: ProductDetailViewModel by viewModels()
    private var product: ProductDetail? = null
    private var menu: Menu? = null

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // Toolbar menu: wishlist heart + share (MenuProvider replaces setHasOptionsMenu)
        requireActivity().addMenuProvider(object : MenuProvider {
            override fun onCreateMenu(m: Menu, inflater: MenuInflater) {
                inflater.inflate(R.menu.menu_product_detail, m)
                menu = m
                updateHeart()
            }

            override fun onMenuItemSelected(item: MenuItem): Boolean = when (item.itemId) {
                R.id.action_wishlist -> { vm.toggleWishlist(); true }
                R.id.action_share -> { share(); true }
                else -> false
            }
        }, viewLifecycleOwner, Lifecycle.State.RESUMED)

        val similar = ProductAdapter(
            fixedWidthDp = 150,
            onClick = { findNavController().navigate(R.id.productDetailFragment, bundleOf(ARG_PRODUCT_ID to it.id)) },
            onWishlist = { }
        )
        binding.similarList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
        binding.similarList.adapter = similar

        binding.minus.setOnClickListener { vm.changeQty(-1) }
        binding.plus.setOnClickListener { vm.changeQty(+1) }
        binding.addToCart.setOnClickListener { vm.addToCart(buyNow = false) }
        binding.buyNow.setOnClickListener { vm.addToCart(buyNow = true) }
        binding.descriptionToggle.setOnClickListener {
            val expanded = binding.description.maxLines == Int.MAX_VALUE
            binding.description.maxLines = if (expanded) 4 else Int.MAX_VALUE
            binding.descriptionToggle.setText(if (expanded) R.string.read_more else R.string.read_less)
        }

        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 -> {
                    binding.state.done(); binding.content.show(); binding.bottomBar.show()
                    bind(state.data)
                    similar.submitList(state.data.similar)
                }
            }
        }
        collectWhenStarted(vm.qty) { binding.qty.text = it.toString() }
        collectWhenStarted(vm.busy) { busy ->
            binding.addToCart.isEnabled = !busy
            binding.buyNow.isEnabled = !busy
        }
        collectWhenStarted(vm.events) { e ->
            when (e) {
                is ProductDetailViewModel.Event.Message -> snack(e.text)
                ProductDetailViewModel.Event.GoToCart -> findNavController().navigate(R.id.cartFragment)
            }
        }
    }

    private fun bind(p: ProductDetail) {
        val firstTime = product == null
        product = p
        (requireActivity() as AppCompatActivity).supportActionBar?.title = p.categoryName

        if (firstTime) {
            binding.gallery.adapter = GalleryAdapter(p.images)
            TabLayoutMediator(binding.galleryDots, binding.gallery) { _, _ -> }.attach()
            binding.galleryDots.visibleIf(p.images.size > 1)
        }
        binding.name.text = p.name
        binding.shortDesc.text = p.shortDesc
        binding.ratingChip.text = getString(R.string.rating_value, p.rating)
        binding.ratingCount.text = getString(R.string.ratings_reviews, p.ratingCount)
        binding.price.text = Format.price(p.price)
        binding.mrp.strike(Format.price(p.mrp))
        binding.mrp.visibleIf(p.mrp > p.price)
        binding.discount.text = getString(R.string.percent_off, p.discountPct)
        binding.discount.visibleIf(p.discountPct > 0)

        val inStock = p.stock > 0
        binding.stock.text = when {
            !inStock -> getString(R.string.out_of_stock)
            p.stock <= 5 -> getString(R.string.only_left, p.stock)
            else -> getString(R.string.in_stock)
        }
        binding.stock.setTextColor(ContextCompat.getColor(requireContext(), if (p.stock in 1..5 || !inStock) R.color.sale else R.color.success))
        binding.qtyRow.visibleIf(inStock)
        binding.addToCart.isEnabled = inStock
        binding.buyNow.isEnabled = inStock
        binding.inCart.visibleIf(p.cartQty > 0)
        binding.inCart.text = getString(R.string.already_in_cart, p.cartQty)

        binding.description.text = p.description
        binding.descriptionToggle.visibleIf((p.description?.length ?: 0) > 180)

        binding.reviewsTitle.text = getString(R.string.reviews_title, p.ratingCount)
        binding.reviewList.layoutManager = LinearLayoutManager(requireContext())
        binding.reviewList.adapter = ReviewAdapter(p.reviews)
        binding.noReviews.visibleIf(p.reviews.isEmpty())
        binding.similarTitle.visibleIf(p.similar.isNotEmpty())
        updateHeart()
    }

    private fun updateHeart() {
        val item = menu?.findItem(R.id.action_wishlist) ?: return
        val inWishlist = product?.inWishlist == true
        item.setIcon(if (inWishlist) R.drawable.ic_heart else R.drawable.ic_heart_outline)
        item.setTitle(if (inWishlist) R.string.remove_from_wishlist else R.string.add_to_wishlist)
    }

    private fun share() {
        val p = product ?: return
        val text = getString(R.string.share_product, p.name, Format.price(p.price))
        startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, text), null))
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
        menu = null
        product = null
    }
}
stockTextColourButtons
0Out of stockreddisabled, quantity hidden
1 – 5Hurry, only 3 left!redenabled
6+In stockgreenenabled

The heart and share icons sit in the toolbar. addMenuProvider(…, viewLifecycleOwner, RESUMED) is the modern replacement for setHasOptionsMenu: the menu is added when the screen is shown and removed automatically when the user leaves.

XML – menu/menu_product_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <item
        android:id="@+id/action_wishlist"
        android:icon="@drawable/ic_heart_outline"
        android:title="@string/add_to_wishlist"
        app:iconTint="@color/on_brand"
        app:showAsAction="always" />
    <item
        android:id="@+id/action_share"
        android:icon="@drawable/ic_share"
        android:title="@string/share"
        app:iconTint="@color/on_brand"
        app:showAsAction="ifRoom" />
</menu>

Add to cart vs Buy now

Add to cart

  • POST cart/add.php {product_id, qty}
  • Snackbar "Added to cart."
  • stay on the product
  • badge on the Cart tab updates

Buy now

  • same API call
  • then Event.GoToCart
  • navigate to the Cart screen
  • user goes straight to checkout
One function, addToCart(buyNow), two behaviours

Small details that matter

  • "Read more" appears only for descriptions longer than 180 characters.
  • "1 already in your cart" tells the user they added it before.
  • Share builds a short text: "Check out Smart Watch Pro for just ₹1,899 on KartEasy!"
  • Tapping a similar product opens another detail screen on top — Back returns to the previous one.
  • The toolbar shows the category name, the product name is the big title in the content.

Frequently Asked Questions

Why is the bottom bar outside the scroll view?
So "Add to cart" and "Buy now" are always visible, wherever the user has scrolled. ConstraintLayout puts the scroll view between the top and the bar.
How can I add colours or sizes (variants)?
Add a product_variants table (product_id, name, price, stock), return the list in product.php, show it as a ChipGroup, and send variant_id with cart/add.php. Stock and price then come from the variant row.
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

Next topicCart Screen Your progress: 20 / 28 (71%) · Lesson 21 / 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