Product List, Search और Filter
Screen
यही 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 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 नए के बाद कभी न दिखें।
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 कर देता है।
Endless scrolling
अगला 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 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>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 खुल जाता है।
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 के साथ दिखाता है।
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 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>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
}
}