Product List, Search & Filter
The screen
The same screen is used for search, every category, and every "See all" link — only the arguments change (categoryId, title, sort, focusSearch).
Step 1: fragment_product_list.xml
At the top: a dense search field with a clear button, a horizontally scrolling ChipGroup (single selection, one always required) and the Filter button. Below: the result count and the grid with a small "loading more" spinner at the bottom.
<?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
Two StateFlows: filters (what the user asked for) and state (what is on screen: items, total, loading, loadingMore, error, hasMore). Any filter change calls reload(), which cancels a running request first, so results of an old search can never appear after a new one.
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)
}
}
}
}Live search with debounce
Typing "watch" fires five text changes. Calling the API five times is wasteful, so search() waits 400 ms: each new letter cancels the previous wait.
Endless scrolling
If loading the next page fails, we go back one page and show a Snackbar; the items already loaded stay on screen.
Step 3: Price filter bottom sheet
A BottomSheetDialogFragment with a two-thumb RangeSlider (₹0 – ₹20,000, steps of ₹500). The result is sent back with the Fragment Result API — no interfaces or shared ViewModel needed. A maximum of ₹20,000 means "any price" and is sent as 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
The sort chips are mapped to API values in one SORTS map. The title in the toolbar comes from the title argument (for example the category name). When opened from the Home search bar, the field gets focus and the keyboard opens.
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
One adapter for every product list. fixedWidthDp gives horizontal rows equal card widths; in grids the card fills its column. The "+" button is hidden for out-of-stock items, and the heart shows the filled or outline icon with a matching content description for TalkBack.
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
A grid of category tiles (3 columns on phones, 5 on tablets) from categories.php. A tap opens the product list for that category. The small ViewModel lives in the same file because it is only used here.
<?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
}
}