Home: Banners, Categories, Products
The home design
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 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 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>Step 3: item layouts
<?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 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.
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) }
}
}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.
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)
}
}
}
}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.
package com.codingeasily.karteasy.ui.home
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.viewpager2.widget.ViewPager2
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.api.UiState
import com.codingeasily.karteasy.data.model.Banner
import com.codingeasily.karteasy.data.model.Category
import com.codingeasily.karteasy.data.model.Product
import com.codingeasily.karteasy.databinding.FragmentHomeBinding
import com.codingeasily.karteasy.ui.category.CategoryAdapter
import com.codingeasily.karteasy.ui.product.ProductAdapter
import com.codingeasily.karteasy.ui.product.ProductDetailFragment
import com.codingeasily.karteasy.ui.product.ProductListFragment
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.done
import com.codingeasily.karteasy.util.error
import com.codingeasily.karteasy.util.hide
import com.codingeasily.karteasy.util.loading
import com.codingeasily.karteasy.util.show
import com.codingeasily.karteasy.util.snack
import com.google.android.material.tabs.TabLayoutMediator
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
private val vm: HomeViewModel by viewModels()
private lateinit var bannerAdapter: BannerAdapter
private val autoScroll = Handler(Looper.getMainLooper())
private val nextBanner = object : Runnable {
override fun run() {
val pager = _binding?.bannerPager ?: return
val count = pager.adapter?.itemCount ?: 0
if (count > 1) pager.currentItem = (pager.currentItem + 1) % count
autoScroll.postDelayed(this, 4000)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
bannerAdapter = BannerAdapter(::openBanner)
binding.bannerPager.adapter = bannerAdapter
TabLayoutMediator(binding.bannerDots, binding.bannerPager) { _, _ -> }.attach()
binding.bannerPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrollStateChanged(state: Int) {
// The user is swiping: restart the 4 second timer.
if (state == ViewPager2.SCROLL_STATE_DRAGGING) restartAutoScroll()
}
})
val categoryAdapter = CategoryAdapter(showCount = false, onClick = ::openCategory)
binding.categoryList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.categoryList.adapter = categoryAdapter
val featured = rowAdapter()
binding.featuredList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.featuredList.adapter = featured
val deals = rowAdapter()
binding.dealsList.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false)
binding.dealsList.adapter = deals
val latest = ProductAdapter(onClick = ::openProduct, onWishlist = { vm.toggleWishlist(it.id) }, onAddToCart = { vm.addToCart(it.id) })
binding.newList.layoutManager = GridLayoutManager(requireContext(), 2)
binding.newList.isNestedScrollingEnabled = false
binding.newList.adapter = latest
binding.searchBar.setOnClickListener {
findNavController().navigate(R.id.productListFragment, bundleOf(ProductListFragment.ARG_FOCUS_SEARCH to true))
}
binding.featuredMore.setOnClickListener { openList(getString(R.string.featured), "popular") }
binding.dealsMore.setOnClickListener { openList(getString(R.string.top_deals), "discount") }
binding.newMore.setOnClickListener { openList(getString(R.string.new_arrivals), "newest") }
binding.categoriesMore.setOnClickListener { findNavController().navigate(R.id.categoriesFragment) }
binding.swipeRefresh.setOnRefreshListener { vm.load() }
collectWhenStarted(vm.state) { state ->
when (state) {
is UiState.Loading -> { binding.content.hide(); binding.state.loading() }
is UiState.Error -> {
binding.swipeRefresh.isRefreshing = false
binding.content.hide()
binding.state.error(state.message) { vm.load() }
}
is UiState.Success -> {
binding.swipeRefresh.isRefreshing = false
binding.state.done()
binding.content.show()
bannerAdapter.items = state.data.banners
categoryAdapter.submitList(state.data.categories)
featured.submitList(state.data.featured)
deals.submitList(state.data.deals)
latest.submitList(state.data.newArrivals)
}
}
}
collectWhenStarted(vm.messages) { snack(it) }
}
private fun rowAdapter() = ProductAdapter(
fixedWidthDp = 164,
onClick = ::openProduct,
onWishlist = { vm.toggleWishlist(it.id) },
onAddToCart = { vm.addToCart(it.id) }
)
private fun openProduct(p: Product) {
findNavController().navigate(R.id.productDetailFragment, bundleOf(ProductDetailFragment.ARG_PRODUCT_ID to p.id))
}
private fun openCategory(c: Category) {
findNavController().navigate(
R.id.productListFragment,
bundleOf(ProductListFragment.ARG_CATEGORY_ID to c.id, ProductListFragment.ARG_TITLE to c.name)
)
}
private fun openList(title: String, sort: String) {
findNavController().navigate(
R.id.productListFragment,
bundleOf(ProductListFragment.ARG_TITLE to title, ProductListFragment.ARG_SORT to sort)
)
}
private fun openBanner(b: Banner) {
when (b.linkType) {
"category" -> b.linkId?.let { openCategory(Category(it, b.title, null)) }
"product" -> b.linkId?.let {
findNavController().navigate(R.id.productDetailFragment, bundleOf(ProductDetailFragment.ARG_PRODUCT_ID to it))
}
}
}
private fun restartAutoScroll() {
autoScroll.removeCallbacks(nextBanner)
autoScroll.postDelayed(nextBanner, 4000)
}
override fun onResume() {
super.onResume()
restartAutoScroll()
}
override fun onPause() {
super.onPause()
autoScroll.removeCallbacks(nextBanner)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}| Tap | Opens |
|---|---|
| Search bar | productListFragment, keyboard open |
| Banner (category / product) | product list of that category / product detail |
| Category icon | product 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
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.
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()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() }
}