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

Product List, Search और Filter

Screen

9:41▮▮ 85%←Electronics⋮🔍 Search for products ✕Price: low to highNewest⚙ Filter (on)3 results🎧Earbuds X2₹1,099🔋Power Bank₹1,499⌚Smart Watch Pro₹1,8999:41▮▮ 85%←Electronics⋮🎧Earbuds X2₹1,099🔋Power Bank₹1,499Filter by price₹0 – ₹2,000●━━━━━━━━━━━━●─────────────ApplyReset9:41▮▮ 85%☰Categories⋮📱Mobiles3 products👗Fashion3 products⌚Electronics4 products🍳Home & Kitchen2 products📘Books2 products🏏Sports2 products🏠Home🗂️Categories♡Wishlist🛒Cart👤Account
Sort chips वाली product list, price filter bottom sheet, और Categories tab

यही screen search, हर category और हर "See all" link के लिए use होती है — सिर्फ arguments बदलते हैं (categoryId, title, sort, focusSearch)।

Step 1: fragment_product_list.xml

ऊपर: clear button वाला dense search field, horizontally scroll होने वाला ChipGroup (single selection, एक हमेशा ज़रूरी) और Filter button। नीचे: result count और grid, जिसके नीचे छोटा "loading more" spinner।

XML – layout/fragment_product_list.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical">

    <!-- Search + sort + filter -->
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@color/surface"
        android:elevation="2dp"
        android:orientation="vertical"
        android:paddingTop="8dp">

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/search_layout"
            style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginEnd="16dp"
            android:hint="@string/search_hint"
            app:boxCornerRadiusBottomEnd="12dp"
            app:boxCornerRadiusBottomStart="12dp"
            app:boxCornerRadiusTopEnd="12dp"
            app:boxCornerRadiusTopStart="12dp"
            app:endIconMode="clear_text"
            app:startIconDrawable="@drawable/ic_search">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/search_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:imeOptions="actionSearch"
                android:inputType="text"
                android:maxLines="1" />
        </com.google.android.material.textfield.TextInputLayout>

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

            <HorizontalScrollView
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:scrollbars="none">

                <com.google.android.material.chip.ChipGroup
                    android:id="@+id/sort_chips"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:paddingStart="16dp"
                    android:paddingEnd="8dp"
                    app:selectionRequired="true"
                    app:singleLine="true"
                    app:singleSelection="true">

                    <com.google.android.material.chip.Chip
                        android:id="@+id/sort_newest"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/sort_newest" />

                    <com.google.android.material.chip.Chip
                        android:id="@+id/sort_popular"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/sort_popular" />

                    <com.google.android.material.chip.Chip
                        android:id="@+id/sort_price_low"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/sort_price_low" />

                    <com.google.android.material.chip.Chip
                        android:id="@+id/sort_price_high"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/sort_price_high" />

                    <com.google.android.material.chip.Chip
                        android:id="@+id/sort_rating"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/sort_rating" />

                    <com.google.android.material.chip.Chip
                        android:id="@+id/sort_discount"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/sort_discount" />
                </com.google.android.material.chip.ChipGroup>
            </HorizontalScrollView>

            <com.google.android.material.button.MaterialButton
                android:id="@+id/filter_button"
                style="@style/Widget.Material3.Button.TextButton.Icon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="8dp"
                android:text="@string/filter"
                app:icon="@drawable/ic_filter" />
        </LinearLayout>
    </LinearLayout>

    <TextView
        android:id="@+id/result_count"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:paddingStart="16dp"
        android:paddingTop="8dp"
        android:paddingEnd="16dp"
        android:textColor="@color/text_secondary"
        android:textSize="13sp"
        tools:text="16 results" />

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:paddingStart="8dp"
            android:paddingEnd="8dp"
            android:paddingBottom="56dp"
            tools:listitem="@layout/item_product" />

        <com.google.android.material.progressindicator.CircularProgressIndicator
            android:id="@+id/load_more"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|center_horizontal"
            android:layout_marginBottom="12dp"
            android:indeterminate="true"
            android:visibility="gone" />

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

Step 2: ProductListViewModel

दो StateFlows: filters (user ने क्या माँगा) और state (screen पर क्या है: items, total, loading, loadingMore, error, hasMore)। कोई भी filter बदलने पर reload() चलता है, जो पहले चल रही request cancel करता है, ताकि पुराने search के results नए के बाद कभी न दिखें।

Kotlin – ui/product/ProductListViewModel.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.model.Product
import com.codingeasily.karteasy.util.kartApp
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

/**
 * Search + filter + sort + "load more" paging.
 * Fragment arguments arrive automatically in SavedStateHandle.
 */
class ProductListViewModel(app: Application, args: SavedStateHandle) : AndroidViewModel(app) {

    data class Filters(
        val query: String = "",
        val categoryId: Int = 0,
        val minPrice: Int = 0,
        val maxPrice: Int = 0,
        val sort: String = "newest"
    )

    data class ListState(
        val items: List<Product> = emptyList(),
        val total: Int = 0,
        val loading: Boolean = false,
        val loadingMore: Boolean = false,
        val error: String? = null,
        val hasMore: Boolean = true
    )

    private val repo = app.kartApp.repository

    private val _filters = MutableStateFlow(
        Filters(
            categoryId = args.get<Int>(ProductListFragment.ARG_CATEGORY_ID) ?: 0,
            sort = args.get<String>(ProductListFragment.ARG_SORT) ?: "newest"
        )
    )
    val filters: StateFlow<Filters> = _filters

    private val _state = MutableStateFlow(ListState())
    val state: StateFlow<ListState> = _state

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

    private var page = 1
    private var job: Job? = null
    private var searchJob: Job? = null

    init {
        reload()
    }

    /** Called on every key press: waits 400 ms so we don't call the API for each letter. */
    fun search(text: String) {
        if (text.trim() == _filters.value.query) return
        searchJob?.cancel()
        searchJob = viewModelScope.launch {
            delay(400)
            _filters.value = _filters.value.copy(query = text.trim())
            reload()
        }
    }

    fun setSort(sort: String) {
        if (sort == _filters.value.sort) return
        _filters.value = _filters.value.copy(sort = sort)
        reload()
    }

    fun setPrice(min: Int, max: Int) {
        _filters.value = _filters.value.copy(minPrice = min, maxPrice = max)
        reload()
    }

    fun reload() {
        job?.cancel()
        page = 1
        _state.value = ListState(loading = true)
        job = viewModelScope.launch { fetch(reset = true) }
    }

    /** Called by the RecyclerView scroll listener near the end of the list. */
    fun loadMore() {
        val s = _state.value
        if (s.loading || s.loadingMore || !s.hasMore || s.error != null) return
        _state.value = s.copy(loadingMore = true)
        page++
        job = viewModelScope.launch { fetch(reset = false) }
    }

    private suspend fun fetch(reset: Boolean) {
        val f = _filters.value
        val r = repo.products(
            query = f.query.ifEmpty { null },
            categoryId = f.categoryId.takeIf { it > 0 },
            minPrice = f.minPrice.takeIf { it > 0 },
            maxPrice = f.maxPrice.takeIf { it > 0 },
            sort = f.sort,
            page = page
        )
        val old = _state.value
        _state.value = when (r) {
            is Result.Success -> ListState(
                items = if (reset) r.data.items else old.items + r.data.items,
                total = r.data.total,
                hasMore = r.data.hasMore
            )
            is Result.Error -> {
                if (reset) ListState(error = r.message)
                else {
                    page--
                    _messages.emit(r.message)
                    old.copy(loadingMore = false)
                }
            }
        }
    }

    fun toggleWishlist(productId: Int) {
        viewModelScope.launch {
            when (val r = repo.toggleWishlist(productId)) {
                is Result.Success -> {
                    _state.value = _state.value.copy(items = _state.value.items.withWishlist(productId, r.data.inWishlist))
                    _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)
            }
        }
    }
}

Debounce के साथ live search

"watch" type करने पर पाँच text changes होते हैं। API को पाँच बार call करना बर्बादी है, इसलिए search() 400 ms रुकता है: हर नया letter पिछला इंतज़ार cancel कर देता है।

KeyboardViewModelAPI"w" → 400 ms timer शुरू"wa" → cancel, timer फिर शुरू"watch" → cancel, timer फिर शुरू400 ms शांति…GET products.php?q=watch (सिर्फ एक call)
Debounce: user के रुकने के बाद एक request

Endless scrolling

onScrolledlast visible ≥ count − 4loadMore()loading / और नहीं / error हो तो skippage++GET …&page=2items = पुराने + नएhasMore API से
User के अंत तक पहुँचने से पहले अगले 20 load

अगला page load fail हो तो हम एक page पीछे जाते हैं और Snackbar दिखाते हैं; पहले से load items screen पर रहते हैं।

Step 3: Price filter bottom sheet

दो thumbs वाले RangeSlider (₹0 – ₹20,000, ₹500 के steps) के साथ एक BottomSheetDialogFragment। Result Fragment Result API से वापस भेजा जाता है — कोई interface या shared ViewModel नहीं चाहिए। ₹20,000 maximum का मतलब "कोई भी price" है और 0 की तरह भेजा जाता है।

XML – layout/sheet_filter.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:padding="24dp">

    <com.google.android.material.bottomsheet.BottomSheetDragHandleView
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/filter_title"
        android:textAppearance="@style/TextAppearance.KartEasy.Section" />

    <TextView
        android:id="@+id/range_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:textColor="@color/brand"
        android:textStyle="bold"
        tools:text="₹0 – ₹5,000" />

    <!-- Two thumbs: minimum and maximum price -->
    <com.google.android.material.slider.RangeSlider
        android:id="@+id/price_slider"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp" />

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

        <com.google.android.material.button.MaterialButton
            android:id="@+id/reset"
            style="@style/Widget.KartEasy.Button.Outlined"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:layout_weight="1"
            android:text="@string/reset" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/apply"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_weight="1"
            android:text="@string/apply" />
    </LinearLayout>
</LinearLayout>
Kotlin – ui/product/FilterSheet.kt
package com.codingeasily.karteasy.ui.product

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.databinding.SheetFilterBinding
import com.codingeasily.karteasy.util.Format
import com.google.android.material.bottomsheet.BottomSheetDialogFragment

/** Bottom sheet with a price RangeSlider. Sends the result back with setFragmentResult. */
class FilterSheet : BottomSheetDialogFragment() {

    companion object {
        const val RESULT = "filter_result"
        const val KEY_MIN = "min"
        const val KEY_MAX = "max"
        private const val MAX_PRICE = 20000f

        fun newInstance(min: Int, max: Int) = FilterSheet().apply {
            arguments = bundleOf(KEY_MIN to min, KEY_MAX to max)
        }
    }

    private var _binding: SheetFilterBinding? = null
    private val binding get() = _binding!!

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val min = requireArguments().getInt(KEY_MIN).toFloat()
        val max = requireArguments().getInt(KEY_MAX).toFloat().takeIf { it > 0 } ?: MAX_PRICE
        binding.priceSlider.valueFrom = 0f
        binding.priceSlider.valueTo = MAX_PRICE
        binding.priceSlider.stepSize = 500f
        binding.priceSlider.values = listOf(min.coerceIn(0f, MAX_PRICE), max.coerceIn(0f, MAX_PRICE))
        binding.priceSlider.setLabelFormatter { Format.price(it.toDouble()) }
        showRange()
        binding.priceSlider.addOnChangeListener { _, _, _ -> showRange() }

        binding.reset.setOnClickListener { send(0, 0) }
        binding.apply.setOnClickListener {
            val v = binding.priceSlider.values
            send(v[0].toInt(), if (v[1] >= MAX_PRICE) 0 else v[1].toInt())
        }
    }

    private fun showRange() {
        val v = binding.priceSlider.values
        val upper = if (v[1] >= MAX_PRICE) getString(R.string.price_any) else Format.price(v[1].toDouble())
        binding.rangeText.text = getString(R.string.price_range, Format.price(v[0].toDouble()), upper)
    }

    private fun send(min: Int, max: Int) {
        setFragmentResult(RESULT, bundleOf(KEY_MIN to min, KEY_MAX to max))
        dismiss()
    }

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

Step 4: ProductListFragment

Sort chips एक SORTS map में API values से जुड़े हैं। Toolbar का title title argument से आता है (जैसे category का नाम)। Home search bar से खोलने पर field को focus मिलता है और keyboard खुल जाता है।

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

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import androidx.core.os.bundleOf
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.databinding.FragmentProductListBinding
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.done
import com.codingeasily.karteasy.util.empty
import com.codingeasily.karteasy.util.error
import com.codingeasily.karteasy.util.loading
import com.codingeasily.karteasy.util.snack
import com.codingeasily.karteasy.util.visibleIf

class ProductListFragment : Fragment() {

    companion object {
        const val ARG_CATEGORY_ID = "categoryId"
        const val ARG_TITLE = "title"
        const val ARG_SORT = "sort"
        const val ARG_FOCUS_SEARCH = "focusSearch"

        /** Chip id -> API sort value */
        val SORTS = mapOf(
            R.id.sort_newest to "newest",
            R.id.sort_popular to "popular",
            R.id.sort_price_low to "price_asc",
            R.id.sort_price_high to "price_desc",
            R.id.sort_rating to "rating",
            R.id.sort_discount to "discount"
        )
    }

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

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        arguments?.getString(ARG_TITLE)?.let { (requireActivity() as AppCompatActivity).supportActionBar?.title = it }

        val adapter = ProductAdapter(
            onClick = { findNavController().navigate(R.id.productDetailFragment, bundleOf(ProductDetailFragment.ARG_PRODUCT_ID to it.id)) },
            onWishlist = { vm.toggleWishlist(it.id) },
            onAddToCart = { vm.addToCart(it.id) }
        )
        val grid = GridLayoutManager(requireContext(), resources.getInteger(R.integer.product_columns))
        binding.list.layoutManager = grid
        binding.list.adapter = adapter

        // Endless scrolling: when 4 items are left, ask for the next page.
        binding.list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
                if (dy > 0 && grid.findLastVisibleItemPosition() >= adapter.itemCount - 4) vm.loadMore()
            }
        })

        // Search box
        binding.searchInput.doAfterTextChanged { vm.search(it?.toString().orEmpty()) }
        binding.searchInput.setOnEditorActionListener { v, actionId, _ ->
            if (actionId == EditorInfo.IME_ACTION_SEARCH) {
                requireContext().getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(v.windowToken, 0)
                true
            } else false
        }
        if (arguments?.getBoolean(ARG_FOCUS_SEARCH) == true && savedInstanceState == null) {
            binding.searchInput.requestFocus()
            binding.searchInput.post {
                requireContext().getSystemService<InputMethodManager>()?.showSoftInput(binding.searchInput, InputMethodManager.SHOW_IMPLICIT)
            }
        }

        // Sort chips: select the current one, then listen for changes.
        val current = SORTS.entries.firstOrNull { it.value == vm.filters.value.sort }?.key ?: R.id.sort_newest
        binding.sortChips.check(current)
        binding.sortChips.setOnCheckedStateChangeListener { _, ids ->
            ids.firstOrNull()?.let { id -> SORTS[id]?.let { vm.setSort(it) } }
        }

        binding.filterButton.setOnClickListener {
            val f = vm.filters.value
            FilterSheet.newInstance(f.minPrice, f.maxPrice).show(childFragmentManager, "filter")
        }
        childFragmentManager.setFragmentResultListener(FilterSheet.RESULT, viewLifecycleOwner) { _, b ->
            vm.setPrice(b.getInt(FilterSheet.KEY_MIN), b.getInt(FilterSheet.KEY_MAX))
        }

        collectWhenStarted(vm.filters) { f ->
            val active = f.minPrice > 0 || f.maxPrice > 0
            binding.filterButton.text = getString(if (active) R.string.filter_on else R.string.filter)
        }

        collectWhenStarted(vm.state) { s ->
            adapter.submitList(s.items)
            binding.loadMore.visibleIf(s.loadingMore)
            binding.resultCount.visibleIf(!s.loading && s.error == null)
            binding.resultCount.text = resources.getQuantityString(R.plurals.results_count, s.total, s.total)
            when {
                s.loading -> binding.state.loading()
                s.error != null -> binding.state.error(s.error) { vm.reload() }
                s.items.isEmpty() -> binding.state.empty(R.drawable.ic_search, R.string.no_products)
                else -> binding.state.done()
            }
        }
        collectWhenStarted(vm.messages) { snack(it) }
    }

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

Step 5: ProductAdapter

हर product list के लिए एक adapter। fixedWidthDp horizontal rows में cards की चौड़ाई बराबर रखता है; grids में card अपना column भरता है। Out-of-stock items पर "+" button छुप जाता है, और heart भरा या outline icon TalkBack के लिए सही content description के साथ दिखाता है।

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

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.Product
import com.codingeasily.karteasy.databinding.ItemProductBinding
import com.codingeasily.karteasy.util.Format
import com.codingeasily.karteasy.util.load
import com.codingeasily.karteasy.util.strike
import com.codingeasily.karteasy.util.visibleIf

/**
 * One adapter for every product list: home rows, category grid, search results, wishlist.
 * fixedWidthDp > 0 is used for horizontal rows so cards keep the same size.
 */
class ProductAdapter(
    private val fixedWidthDp: Int = 0,
    private val onClick: (Product) -> Unit,
    private val onWishlist: (Product) -> Unit,
    private val onAddToCart: ((Product) -> Unit)? = null
) : ListAdapter<Product, ProductAdapter.Holder>(DIFF) {

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

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val b = ItemProductBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        if (fixedWidthDp > 0) {
            b.root.layoutParams = b.root.layoutParams.apply {
                width = (fixedWidthDp * parent.resources.displayMetrics.density).toInt()
            }
        }
        return Holder(b)
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val p = getItem(position)
        val b = holder.b
        b.image.load(p.image)
        b.name.text = p.name
        b.price.text = Format.price(p.price)
        b.mrp.strike(Format.price(p.mrp))
        b.mrp.visibleIf(p.mrp > p.price)
        b.discount.text = b.root.context.getString(R.string.percent_off, p.discountPct)
        b.discount.visibleIf(p.discountPct > 0)
        b.rating.text = String.format("%.1f", p.rating)
        b.ratingCount.text = b.root.context.getString(R.string.rating_count, p.ratingCount)
        b.outOfStock.visibleIf(p.stock <= 0)
        b.wishlist.setImageResource(if (p.inWishlist) R.drawable.ic_heart else R.drawable.ic_heart_outline)
        b.wishlist.contentDescription = b.root.context.getString(
            if (p.inWishlist) R.string.remove_from_wishlist else R.string.add_to_wishlist
        )
        b.wishlist.setOnClickListener { onWishlist(p) }
        b.root.setOnClickListener { onClick(p) }
        b.addButton.visibleIf(onAddToCart != null && p.stock > 0)
        b.addButton.setOnClickListener { onAddToCart?.invoke(p) }
    }

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

Step 6: Categories tab

categories.php से category tiles का grid (phones पर 3 columns, tablets पर 5)। Tap करने पर उस category की product list खुलती है। छोटा ViewModel इसी file में है क्योंकि सिर्फ यहीं use होता है।

XML – layout/fragment_categories.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

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

    <include
        android:id="@+id/state"
        layout="@layout/layout_state" />
</FrameLayout>
Kotlin – ui/category/CategoriesFragment.kt
package com.codingeasily.karteasy.ui.category

import android.app.Application
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.api.Result
import com.codingeasily.karteasy.data.api.UiState
import com.codingeasily.karteasy.data.model.Category
import com.codingeasily.karteasy.databinding.FragmentCategoriesBinding
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.kartApp
import com.codingeasily.karteasy.util.loading
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class CategoriesViewModel(app: Application) : AndroidViewModel(app) {
    private val repo = app.kartApp.repository
    private val _state = MutableStateFlow<UiState<List<Category>>>(UiState.Loading)
    val state: StateFlow<UiState<List<Category>>> = _state

    init { load() }

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

class CategoriesFragment : Fragment() {

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

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val adapter = CategoryAdapter(showCount = true) { c ->
            findNavController().navigate(
                R.id.productListFragment,
                bundleOf(ProductListFragment.ARG_CATEGORY_ID to c.id, ProductListFragment.ARG_TITLE to c.name)
            )
        }
        binding.list.layoutManager = GridLayoutManager(requireContext(), resources.getInteger(R.integer.category_columns))
        binding.list.adapter = adapter

        collectWhenStarted(vm.state) { state ->
            when (state) {
                is UiState.Loading -> binding.state.loading()
                is UiState.Error -> binding.state.error(state.message) { vm.load() }
                is UiState.Success -> {
                    binding.state.done()
                    adapter.submitList(state.data)
                }
            }
        }
    }

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

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

Paging 3 library क्यों नहीं?
बहुत बड़ी lists के लिए Paging 3 बढ़िया है, पर कई नई classes जोड़ता है। यहाँ वाला manual तरीका (page number + has_more + scroll listener) लगभग 30 lines का है, debug करना आसान है और हमारी PHP API के साथ perfect चलता है। बाद में API बदले बिना Paging 3 पर जा सकते हैं।
Arguments ViewModel तक कैसे पहुँचते हैं?
Navigation fragment arguments को ViewModel के SavedStateHandle में अपने आप डाल देता है। ProductListViewModel(app, args: SavedStateHandle) उससे categoryId और sort पढ़ता है, और ये process death में भी बचे रहते हैं।
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

अगला topicProduct Detail Screen आपकी प्रगति: 19 / 28 (68%) · Lesson 20 / 28 आगे बढ़ें →

यह lesson कैसा लगा?

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

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

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

▶ CodingEasily on YouTube

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

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

▶ Channel subscribe करें