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

Home: Banners, Categories, Products

Home का design

9:41▮▮ 85%☰KartEasy⋮🔍 Search for productsFestive SaleElectronics पर 60% तक offShop by categorySee all📱 Mobiles👗 FashionFeatured for youSee all⌚Smart Watch Pro₹1,899👟Running Sneakers₹1,299🏠Home🗂️Categories♡Wishlist🛒Cart👤Account9:41▮▮ 85%☰KartEasy⋮🏷 Top dealsSee all🎧Earbuds X2 63%₹1,099⌚Smart Watch 62%₹1,899New arrivalsSee all🏏Cricket Bat₹3,499🧘Yoga Mat 6mm₹699🏠Home🗂️Categories♡Wishlist🛒Cart👤Account
Home का ऊपरी हिस्सा, और नीचे scroll करने के बाद
Search barTextView जो keyboard के साथ product list खोलता हैBanner sliderViewPager2 + TabLayout dots, हर 4 s slideCategoriesगोल icons की horizontal RecyclerViewFeatured / Top dealshorizontal RecyclerViews, 164dp cardsNew arrivalsscroll view के अंदर 2-column grid
एक NestedScrollView + SwipeRefreshLayout के अंदर sections

Step 1: fragment_home.xml

Root एक FrameLayout है जिसके दो children हैं: scroll होने वाला content और ऊपर shared state view। Section headers theme lesson के styles use करते हैं, जिससे यह file पढ़ने लायक रहती है।

XML – layout/fragment_home.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/swipe_refresh"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.core.widget.NestedScrollView
            android:id="@+id/content"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:paddingBottom="16dp">

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

                <!-- Search bar (opens the product list with the keyboard) -->
                <FrameLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:background="@color/brand"
                    android:paddingStart="16dp"
                    android:paddingEnd="16dp"
                    android:paddingBottom="14dp">

                    <TextView
                        android:id="@+id/search_bar"
                        android:layout_width="match_parent"
                        android:layout_height="48dp"
                        android:background="@drawable/bg_search_bar"
                        android:drawablePadding="12dp"
                        android:focusable="true"
                        android:gravity="center_vertical"
                        android:paddingStart="14dp"
                        android:paddingEnd="14dp"
                        android:text="@string/search_hint"
                        android:textColor="@color/text_secondary"
                        app:drawableStartCompat="@drawable/ic_search" />
                </FrameLayout>

                <!-- Auto-scrolling banners -->
                <androidx.viewpager2.widget.ViewPager2
                    android:id="@+id/banner_pager"
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/banner_height"
                    android:layout_marginTop="12dp" />

                <com.google.android.material.tabs.TabLayout
                    android:id="@+id/banner_dots"
                    android:layout_width="wrap_content"
                    android:layout_height="16dp"
                    android:layout_gravity="center_horizontal"
                    android:layout_marginTop="6dp"
                    android:background="@android:color/transparent"
                    app:tabBackground="@drawable/tab_dot"
                    app:tabGravity="center"
                    app:tabIndicatorHeight="0dp"
                    app:tabMaxWidth="18dp"
                    app:tabMinWidth="18dp"
                    app:tabPaddingEnd="0dp"
                    app:tabPaddingStart="0dp" />

                <!-- Categories -->
                <LinearLayout style="@style/KartEasy.SectionHeader">

                    <TextView
                        style="@style/KartEasy.SectionTitle"
                        android:text="@string/shop_by_category" />

                    <TextView
                        android:id="@+id/categories_more"
                        style="@style/KartEasy.SeeAll" />
                </LinearLayout>

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/category_list"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:clipToPadding="false"
                    android:paddingStart="8dp"
                    android:paddingEnd="8dp"
                    tools:listitem="@layout/item_category" />

                <!-- Featured -->
                <LinearLayout style="@style/KartEasy.SectionHeader">

                    <TextView
                        style="@style/KartEasy.SectionTitle"
                        android:text="@string/featured" />

                    <TextView
                        android:id="@+id/featured_more"
                        style="@style/KartEasy.SeeAll" />
                </LinearLayout>

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/featured_list"
                    style="@style/KartEasy.HorizontalRow"
                    tools:listitem="@layout/item_product" />

                <!-- Deals -->
                <LinearLayout style="@style/KartEasy.SectionHeader">

                    <TextView
                        style="@style/KartEasy.SectionTitle"
                        android:drawablePadding="6dp"
                        android:text="@string/top_deals"
                        app:drawableStartCompat="@drawable/ic_offer"
                        app:drawableTint="@color/sale" />

                    <TextView
                        android:id="@+id/deals_more"
                        style="@style/KartEasy.SeeAll" />
                </LinearLayout>

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/deals_list"
                    style="@style/KartEasy.HorizontalRow"
                    tools:listitem="@layout/item_product" />

                <!-- New arrivals: 2-column grid inside the scroll view -->
                <LinearLayout style="@style/KartEasy.SectionHeader">

                    <TextView
                        style="@style/KartEasy.SectionTitle"
                        android:text="@string/new_arrivals" />

                    <TextView
                        android:id="@+id/new_more"
                        style="@style/KartEasy.SeeAll" />
                </LinearLayout>

                <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/new_list"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:paddingStart="8dp"
                    android:paddingEnd="8dp"
                    tools:listitem="@layout/item_product" />
            </LinearLayout>
        </androidx.core.widget.NestedScrollView>
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

    <include
        android:id="@+id/state"
        layout="@layout/layout_state" />
</FrameLayout>

Step 2: loading / error / empty के लिए एक view

हर list screen को spinner, "Try again" वाला error और empty message चाहिए। इन्हें बार-बार लिखने की जगह layout_state.xml हर जगह <include android:id="@+id/state" …/> से जुड़ता है और चार extension functions से control होता है: loading(), error(), empty(), done()।

XML – layout/layout_state.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- One reusable view for Loading / Error / Empty. Included in every list screen.
     Controlled from Extensions.kt: state.loading(), state.error(...), state.empty(...), state.done() -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?android:attr/colorBackground"
    android:clickable="true"
    android:focusable="true"
    android:gravity="center"
    android:orientation="vertical"
    android:padding="32dp"
    android:visibility="gone">

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/progress"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true" />

    <ImageView
        android:id="@+id/state_icon"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:importantForAccessibility="no"
        android:scaleType="centerInside" />

    <TextView
        android:id="@+id/state_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:gravity="center"
        android:lineSpacingExtra="4dp"
        android:textAppearance="?attr/textAppearanceBodyLarge"
        android:textColor="@color/text_secondary" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/state_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:paddingStart="32dp"
        android:paddingEnd="32dp" />
</LinearLayout>
9:41▮▮ 85%☰KartEasy⋮◌state.loading()9:41▮▮ 85%☰KartEasy⋮☁̸No internet connection. Checkyour network and try again.Try again9:41▮▮ 85%☰Cart⋮🛒Your cart is empty. Addsomething you love!Start shopping
एक ही include तीन states में: loading, error, empty

Step 3: item layouts

XML – layout/item_banner.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="16dp"
    android:layout_marginEnd="16dp"
    app:cardCornerRadius="16dp"
    app:cardElevation="0dp">

    <ImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerCrop" />
</com.google.android.material.card.MaterialCardView>
XML – layout/item_category.xml
<?xml version="1.0" encoding="utf-8"?>
<!-- Category: round image + name (+ product count in the Categories tab) -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="96dp"
    android:layout_height="wrap_content"
    android:layout_margin="4dp"
    android:background="?attr/selectableItemBackground"
    android:gravity="center_horizontal"
    android:orientation="vertical"
    android:padding="6dp">

    <com.google.android.material.imageview.ShapeableImageView
        android:id="@+id/image"
        android:layout_width="72dp"
        android:layout_height="72dp"
        android:importantForAccessibility="no"
        android:scaleType="centerCrop"
        app:shapeAppearanceOverlay="@style/ShapeAppearance.KartEasy.Circle" />

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="6dp"
        android:gravity="center"
        android:maxLines="2"
        android:textColor="@color/text_primary"
        android:textSize="13sp" />

    <TextView
        android:id="@+id/count"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/text_secondary"
        android:textSize="11sp"
        android:visibility="gone" />
</LinearLayout>

Product card item_product.xml Theme lesson में दिखाया गया है और यहाँ reuse होता है।

Step 4: adapters

BannerAdapter एक simple RecyclerView adapter है (ViewPager2 अंदर RecyclerView use करता है)। CategoryAdapter और ProductAdapter ListAdapter extend करते हैं, जो DiffUtil से सिर्फ बदले items animate करता है — जैसे एक heart tap होने पर।

Kotlin – ui/home/BannerAdapter.kt
package com.codingeasily.karteasy.ui.home

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.karteasy.data.model.Banner
import com.codingeasily.karteasy.databinding.ItemBannerBinding
import com.codingeasily.karteasy.util.load

class BannerAdapter(private val onClick: (Banner) -> Unit) : RecyclerView.Adapter<BannerAdapter.Holder>() {

    var items: List<Banner> = emptyList()
        set(value) {
            field = value
            notifyDataSetChanged()
        }

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

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

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val banner = items[position]
        holder.b.image.load(banner.image)
        holder.b.image.contentDescription = banner.title
        holder.b.root.setOnClickListener { onClick(banner) }
    }
}
Kotlin – ui/category/CategoryAdapter.kt
package com.codingeasily.karteasy.ui.category

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.Category
import com.codingeasily.karteasy.databinding.ItemCategoryBinding
import com.codingeasily.karteasy.util.load
import com.codingeasily.karteasy.util.visibleIf

/** Round category icons (home row) or bigger tiles with product count (categories tab). */
class CategoryAdapter(
    private val showCount: Boolean,
    private val onClick: (Category) -> Unit
) : ListAdapter<Category, CategoryAdapter.Holder>(DIFF) {

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

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val b = ItemCategoryBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        // Home row: fixed 96dp tiles. Categories grid: fill the grid cell.
        if (showCount) b.root.layoutParams = b.root.layoutParams.apply { width = ViewGroup.LayoutParams.MATCH_PARENT }
        return Holder(b)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val c = getItem(position)
        holder.b.image.load(c.image)
        holder.b.name.text = c.name
        holder.b.count.visibleIf(showCount)
        holder.b.count.text = holder.b.root.context.resources.getQuantityString(R.plurals.products_count, c.productCount, c.productCount)
        holder.b.root.setOnClickListener { onClick(c) }
    }

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

Step 5: HomeViewModel

एक StateFlow<UiState<HomeData>> पूरी screen रखता है। Messages (snackbars) SharedFlow से जाते हैं ताकि एक बार दिखें और rotation के बाद दोबारा नहीं। Heart tap होने पर Home reload करने की जगह हम withWishlist() से तीनों lists update करते हैं।

Kotlin – ui/home/HomeViewModel.kt
package com.codingeasily.karteasy.ui.home

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.HomeData
import com.codingeasily.karteasy.ui.product.withWishlist
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 HomeViewModel(app: Application) : AndroidViewModel(app) {

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

    /** One-time messages (snackbars). SharedFlow so they are not shown again after rotation. */
    private val _messages = MutableSharedFlow<String>()
    val messages: SharedFlow<String> = _messages

    init {
        load()
    }

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

    fun toggleWishlist(productId: Int) {
        viewModelScope.launch {
            when (val r = repo.toggleWishlist(productId)) {
                is Result.Success -> {
                    val cur = (_state.value as? UiState.Success)?.data ?: return@launch
                    val w = r.data.inWishlist
                    _state.value = UiState.Success(
                        cur.copy(
                            featured = cur.featured.withWishlist(productId, w),
                            deals = cur.deals.withWishlist(productId, w),
                            newArrivals = cur.newArrivals.withWishlist(productId, w)
                        )
                    )
                    _messages.emit(r.message)
                }
                is Result.Error -> _messages.emit(r.message)
            }
        }
    }

    fun addToCart(productId: Int) {
        viewModelScope.launch {
            when (val r = repo.addToCart(productId)) {
                is Result.Success -> _messages.emit(r.message)
                is Result.Error -> _messages.emit(r.message)
            }
        }
    }
}
Kotlin – ui/product/WishlistHelper.kt
package com.codingeasily.karteasy.ui.product

import com.codingeasily.karteasy.data.model.Product

/** After a heart tap, return the same list with that product's in_wishlist flipped. */
fun List<Product>.withWishlist(productId: Int, inWishlist: Boolean): List<Product> =
    map { if (it.id == productId) it.copy(inWishlist = inWishlist) else it }

Step 6: HomeFragment

Fragment adapters और clicks जोड़ता है, फिर हर state बस draw करता है। Navigation destination ids और ProductDetailFragment.ARG_PRODUCT_ID जैसे constants वाले bundleOf() arguments use करता है, ताकि typo से screen न टूटे।

Kotlin – ui/home/HomeFragment.kt
package com.codingeasily.karteasy.ui.home

import android.os.Bundle
import android.os.Handler
import android.os.Looper
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.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.api.UiState
import com.codingeasily.karteasy.data.model.Banner
import com.codingeasily.karteasy.data.model.Category
import com.codingeasily.karteasy.data.model.Product
import com.codingeasily.karteasy.databinding.FragmentHomeBinding
import com.codingeasily.karteasy.ui.category.CategoryAdapter
import com.codingeasily.karteasy.ui.product.ProductAdapter
import com.codingeasily.karteasy.ui.product.ProductDetailFragment
import com.codingeasily.karteasy.ui.product.ProductListFragment
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.google.android.material.tabs.TabLayoutMediator

class HomeFragment : Fragment() {

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

    private lateinit var bannerAdapter: BannerAdapter
    private val autoScroll = Handler(Looper.getMainLooper())
    private val nextBanner = object : Runnable {
        override fun run() {
            val pager = _binding?.bannerPager ?: return
            val count = pager.adapter?.itemCount ?: 0
            if (count > 1) pager.currentItem = (pager.currentItem + 1) % count
            autoScroll.postDelayed(this, 4000)
        }
    }

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        bannerAdapter = BannerAdapter(::openBanner)
        binding.bannerPager.adapter = bannerAdapter
        TabLayoutMediator(binding.bannerDots, binding.bannerPager) { _, _ -> }.attach()
        binding.bannerPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
            override fun onPageScrollStateChanged(state: Int) {
                // The user is swiping: restart the 4 second timer.
                if (state == ViewPager2.SCROLL_STATE_DRAGGING) restartAutoScroll()
            }
        })

        val categoryAdapter = CategoryAdapter(showCount = false, onClick = ::openCategory)
        binding.categoryList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
        binding.categoryList.adapter = categoryAdapter

        val featured = rowAdapter()
        binding.featuredList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
        binding.featuredList.adapter = featured

        val deals = rowAdapter()
        binding.dealsList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
        binding.dealsList.adapter = deals

        val latest = ProductAdapter(onClick = ::openProduct, onWishlist = { vm.toggleWishlist(it.id) }, onAddToCart = { vm.addToCart(it.id) })
        binding.newList.layoutManager = GridLayoutManager(requireContext(), 2)
        binding.newList.isNestedScrollingEnabled = false
        binding.newList.adapter = latest

        binding.searchBar.setOnClickListener {
            findNavController().navigate(R.id.productListFragment, bundleOf(ProductListFragment.ARG_FOCUS_SEARCH to true))
        }
        binding.featuredMore.setOnClickListener { openList(getString(R.string.featured), "popular") }
        binding.dealsMore.setOnClickListener { openList(getString(R.string.top_deals), "discount") }
        binding.newMore.setOnClickListener { openList(getString(R.string.new_arrivals), "newest") }
        binding.categoriesMore.setOnClickListener { findNavController().navigate(R.id.categoriesFragment) }
        binding.swipeRefresh.setOnRefreshListener { vm.load() }

        collectWhenStarted(vm.state) { state ->
            when (state) {
                is UiState.Loading -> { binding.content.hide(); binding.state.loading() }
                is UiState.Error -> {
                    binding.swipeRefresh.isRefreshing = false
                    binding.content.hide()
                    binding.state.error(state.message) { vm.load() }
                }
                is UiState.Success -> {
                    binding.swipeRefresh.isRefreshing = false
                    binding.state.done()
                    binding.content.show()
                    bannerAdapter.items = state.data.banners
                    categoryAdapter.submitList(state.data.categories)
                    featured.submitList(state.data.featured)
                    deals.submitList(state.data.deals)
                    latest.submitList(state.data.newArrivals)
                }
            }
        }
        collectWhenStarted(vm.messages) { snack(it) }
    }

    private fun rowAdapter() = ProductAdapter(
        fixedWidthDp = 164,
        onClick = ::openProduct,
        onWishlist = { vm.toggleWishlist(it.id) },
        onAddToCart = { vm.addToCart(it.id) }
    )

    private fun openProduct(p: Product) {
        findNavController().navigate(R.id.productDetailFragment, bundleOf(ProductDetailFragment.ARG_PRODUCT_ID to p.id))
    }

    private fun openCategory(c: Category) {
        findNavController().navigate(
            R.id.productListFragment,
            bundleOf(ProductListFragment.ARG_CATEGORY_ID to c.id, ProductListFragment.ARG_TITLE to c.name)
        )
    }

    private fun openList(title: String, sort: String) {
        findNavController().navigate(
            R.id.productListFragment,
            bundleOf(ProductListFragment.ARG_TITLE to title, ProductListFragment.ARG_SORT to sort)
        )
    }

    private fun openBanner(b: Banner) {
        when (b.linkType) {
            "category" -> b.linkId?.let { openCategory(Category(it, b.title, null)) }
            "product" -> b.linkId?.let {
                findNavController().navigate(R.id.productDetailFragment, bundleOf(ProductDetailFragment.ARG_PRODUCT_ID to it))
            }
        }
    }

    private fun restartAutoScroll() {
        autoScroll.removeCallbacks(nextBanner)
        autoScroll.postDelayed(nextBanner, 4000)
    }

    override fun onResume() {
        super.onResume()
        restartAutoScroll()
    }

    override fun onPause() {
        super.onPause()
        autoScroll.removeCallbacks(nextBanner)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}
Tapक्या खुलता है
Search barproductListFragment, keyboard खुला
Banner (category / product)उस category की product list / product detail
Category iconcategory से filtered product list
See all (featured / deals / new)popular / discount / newest से sorted product list
Product card · ♡ · (+)detail · wishlist toggle · cart में add

Auto-slider कैसे काम करता है

HandlerViewPager2Userहर 4 s: currentItem = (current + 1) % countswipe शुरू (SCROLL_STATE_DRAGGING)restartAutoScroll(): फिर 4 s रुकेंonPause: removeCallbacks · onResume: start
Slider कभी user से नहीं लड़ता

Helper extensions

छोटे extension functions बहुत सारा दोहराया code हटाते हैं: show()/hide(), placeholder और fade के साथ Glide के लिए load(url), MRP के लिए strike(), snack(), और collectWhenStarted() जो repeatOnLifecycle(STARTED) को wrap करता है। Format.price() Indian rupees print करता है।

Kotlin – util/Extensions.kt
package com.codingeasily.karteasy.util

import android.content.Context
import android.graphics.Paint
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.codingeasily.karteasy.KartApp
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.databinding.LayoutStateBinding
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch

val Context.kartApp: KartApp get() = applicationContext as KartApp

fun View.show() { visibility = View.VISIBLE }
fun View.hide() { visibility = View.GONE }
fun View.visibleIf(condition: Boolean) { visibility = if (condition) View.VISIBLE else View.GONE }

fun Context.toast(message: String) = Toast.makeText(this, message, Toast.LENGTH_SHORT).show()

fun Fragment.snack(message: String) {
    view?.let { Snackbar.make(it, message, Snackbar.LENGTH_SHORT).show() }
}

/** Loads an image URL with a placeholder and a short fade. */
fun ImageView.load(url: String?) {
    Glide.with(this)
        .load(url)
        .placeholder(R.drawable.bg_image_placeholder)
        .error(R.drawable.bg_image_placeholder)
        .transition(DrawableTransitionOptions.withCrossFade(200))
        .into(this)
}

/** Shows "₹4,999" with a line through it (the MRP). */
fun TextView.strike(text: String) {
    this.text = text
    paintFlags = paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
}

/**
 * Collects a Flow only while the Fragment's view is visible (STARTED).
 * Stops automatically when the user leaves the screen: no leaks, no crashes.
 */
fun <T> Fragment.collectWhenStarted(flow: Flow<T>, action: (T) -> Unit) {
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            flow.collect { action(it) }
        }
    }
}

// ---------- the shared loading / error / empty view (layout_state.xml)
fun LayoutStateBinding.loading() {
    root.show(); progress.show(); stateIcon.hide(); stateText.hide(); stateButton.hide()
}

fun LayoutStateBinding.error(message: String, onRetry: () -> Unit) {
    root.show(); progress.hide()
    stateIcon.setImageResource(R.drawable.ic_cloud_off); stateIcon.show()
    stateText.text = message; stateText.show()
    stateButton.setText(R.string.retry); stateButton.show()
    stateButton.setOnClickListener { onRetry() }
}

fun LayoutStateBinding.empty(icon: Int, @StringRes message: Int, @StringRes button: Int? = null, onClick: (() -> Unit)? = null) {
    root.show(); progress.hide()
    stateIcon.setImageResource(icon); stateIcon.show()
    stateText.setText(message); stateText.show()
    if (button != null && onClick != null) {
        stateButton.setText(button); stateButton.show(); stateButton.setOnClickListener { onClick() }
    } else stateButton.hide()
}

fun LayoutStateBinding.done() = root.hide()
Kotlin – util/Format.kt
package com.codingeasily.karteasy.util

import java.text.NumberFormat
import java.text.SimpleDateFormat
import java.util.Locale

/** Money and date formatting in Indian style: ₹1,24,999 */
object Format {

    private val india: Locale = Locale.forLanguageTag("en-IN")

    private val inr: NumberFormat = NumberFormat.getCurrencyInstance(india)

    /** ₹14,999 for whole rupees, ₹2,698.20 when there are paise. */
    fun price(value: Double): String {
        val whole = value % 1.0 == 0.0
        inr.minimumFractionDigits = if (whole) 0 else 2
        inr.maximumFractionDigits = if (whole) 0 else 2
        return inr.format(value)
    }

    private val apiDate = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
    private val niceDate = SimpleDateFormat("dd MMM yyyy, hh:mm a", india)
    private val shortDate = SimpleDateFormat("dd MMM yyyy", india)

    fun date(value: String, withTime: Boolean = true): String = try {
        val d = apiDate.parse(value)
        if (d == null) value else (if (withTime) niceDate else shortDate).format(d)
    } catch (e: Exception) {
        value
    }

    /** "placed" -> "Placed" */
    fun status(value: String): String = value.replaceFirstChar { it.uppercase() }
}
Format.price(14999.0) → ₹14,999 Format.price(124999.5) → ₹1,24,999.50 Format.price(2698.2) → ₹2,698.20 Format.date("2026-10-19 01:02:33") → 19 Oct 2026, 01:02 am

अक्सर पूछे जाने वाले प्रश्न (FAQ)

View types वाला एक बड़ा RecyclerView क्यों नहीं?
बहुत लंबी feeds के लिए वही best है। कुछ fixed sections वाली home के लिए NestedScrollView के साथ छोटे RecyclerViews पढ़ने और बदलने में बहुत आसान हैं, और हर row में सिर्फ 10 items हैं।
Screen छोड़ने के बाद भी banner slide होता रहता है — KartEasy में क्यों नहीं?
Handler callback onPause() में हटता है और onResume() में फिर शुरू होता है, और pager छूने से पहले _binding check करता है, इसलिए destroyed view पर कभी नहीं चलता।
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

अगला topicProduct List, Search और Filter आपकी प्रगति: 18 / 28 (64%) · Lesson 19 / 28 आगे बढ़ें →

यह lesson कैसा लगा?

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

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

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

▶ CodingEasily on YouTube

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

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

▶ Channel subscribe करें