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

Home: Banners, Categories, Products

The home design

9:41▮▮ 85%☰KartEasy⋮🔍 Search for productsFestive SaleUp to 60% off on electronicsShop 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
Top of Home, and after scrolling down
Search barTextView that opens the product list with the keyboardBanner sliderViewPager2 + TabLayout dots, slides every 4 sCategorieshorizontal RecyclerView of round iconsFeatured / Top dealshorizontal RecyclerViews, 164dp cardsNew arrivals2-column grid inside the scroll view
Sections inside one NestedScrollView + SwipeRefreshLayout

Step 1: fragment_home.xml

The root is a FrameLayout with two children: the scrolling content and the shared state view on top. The section headers use the styles from the theme lesson, which keeps this file readable.

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: one view for loading / error / empty

Every list screen needs a spinner, an error with "Try again", and an empty message. Instead of repeating them, layout_state.xml is included everywhere with <include android:id="@+id/state" …/> and controlled by four extension functions: 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
The same include in three 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>

The product card item_product.xml is shown in the Theme lesson and reused here.

Step 4: adapters

BannerAdapter is a simple RecyclerView adapter (ViewPager2 uses RecyclerView inside). CategoryAdapter and ProductAdapter extend ListAdapter, which uses DiffUtil to animate only the items that changed — for example when one heart is tapped.

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

One StateFlow<UiState<HomeData>> holds the whole screen. Messages (snackbars) go through a SharedFlow so they are shown once and not again after rotation. When a heart is tapped we update the three lists with withWishlist() instead of reloading Home.

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

The fragment wires adapters and clicks, then simply draws each state. Navigation uses destination ids plus bundleOf() arguments with constants like ProductDetailFragment.ARG_PRODUCT_ID, so a typo cannot break a 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
    }
}
TapOpens
Search barproductListFragment, keyboard open
Banner (category / product)product list of that category / product detail
Category iconproduct list filtered by category
See all (featured / deals / new)product list sorted popular / discount / newest
Product card · ♡ · (+)detail · toggle wishlist · add to cart

How the auto-slider works

HandlerViewPager2Userevery 4 s: currentItem = (current + 1) % countstarts swiping (SCROLL_STATE_DRAGGING)restartAutoScroll(): wait 4 s againonPause: removeCallbacks · onResume: start
The slider never fights with the user

Helper extensions

Small extension functions remove a lot of repeated code: show()/hide(), load(url) for Glide with placeholder and fade, strike() for the MRP, snack(), and collectWhenStarted() which wraps repeatOnLifecycle(STARTED). Format.price() prints Indian rupees.

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

Frequently Asked Questions

Why not one big RecyclerView with view types?
That is the best choice for very long feeds. For a home with a few fixed sections, a NestedScrollView with small RecyclerViews is much easier to read and change, and each row has only 10 items.
The banner keeps sliding after I leave the screen — why not in KartEasy?
The Handler callback is removed in onPause() and started again in onResume(), and it checks _binding before touching the pager, so it never runs on a 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.

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

Next topicProduct List, Search & Filter Your progress: 18 / 28 (64%) · Lesson 19 / 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