Product Detail Screen
The screen
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 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 & 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 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" />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 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>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.
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.
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
}
}| stock | Text | Colour | Buttons |
|---|---|---|---|
| 0 | Out of stock | red | disabled, quantity hidden |
| 1 – 5 | Hurry, only 3 left! | red | enabled |
| 6+ | In stock | green | enabled |
Toolbar menu with MenuProvider
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 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
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.
