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

Wishlist Screen

Where the heart appears

The heart is on every product card (Home rows, product grids, search, similar products) and in the toolbar of the detail screen. The API sends in_wishlist with every product, so the heart is filled correctly the moment a list appears.

9:41▮▮ 85%☰Wishlist⋮👟♥ Men Running Sneakers₹1,299👗♥ Women Cotton Kurta₹899🧴♥ Steel Water Bottle₹549Tap ♥ to remove · tap + to move tocart🏠Home🗂️Categories♡Wishlist🛒Cart👤Account9:41▮▮ 85%☰Wishlist⋮♡Your wishlist is empty. Tap ♡ onany product to save it.Start shopping🏠Home🗂️Categories♡Wishlist🛒Cart👤Account
Wishlist with items, and the empty state

One toggle, three places

Every screen calls the same repo.toggleWishlist(id), which hits wishlist/toggle.php and returns the new in_wishlist. Each ViewModel then updates only its own data:

Kotlin – the three variants
// 1) Home / product list – flip the flag in the list we already have
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)               // "Added to wishlist."
            }
            is Result.Error -> _messages.emit(r.message)
        }
    }
}

// 2) Product detail – update the detail object and the toolbar heart
_state.value = UiState.Success(cur.copy(inWishlist = r.data.inWishlist))

// 3) Wishlist screen – a heart tap always means "remove" (see WishlistFragment.kt)
Kotlin – ui/product/WishlistHelper.kt
package com.codingeasily.karteasy.ui.product

import com.codingeasily.karteasy.data.model.Product

/** After a heart tap, return the same list with that product's in_wishlist flipped. */
fun List<Product>.withWishlist(productId: Int, inWishlist: Boolean): List<Product> =
    map { if (it.id == productId) it.copy(inWishlist = inWishlist) else it }

Because ProductAdapter is a ListAdapter, giving it a list where one product's inWishlist changed redraws just that one card.

The wishlist screen

onResumeload() wishlist/list.phpgridsame ProductAdapter as everywhere♥ tapremove (optimistic)+ tapadd to cart, then remove from wishlist
What the screen does

Step 1: layout and fragment

XML – layout/fragment_wishlist.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_product" />

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

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.Product
import com.codingeasily.karteasy.databinding.FragmentWishlistBinding
import com.codingeasily.karteasy.ui.product.ProductAdapter
import com.codingeasily.karteasy.ui.product.ProductDetailFragment
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.kartApp
import com.codingeasily.karteasy.util.loading
import com.codingeasily.karteasy.util.snack
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 WishlistViewModel(app: Application) : AndroidViewModel(app) {
    private val repo = app.kartApp.repository
    private val _state = MutableStateFlow<UiState<List<Product>>>(UiState.Loading)
    val state: StateFlow<UiState<List<Product>>> = _state
    private val _messages = MutableSharedFlow<String>()
    val messages: SharedFlow<String> = _messages

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

    /** In the wishlist a heart tap always means "remove". */
    fun remove(p: Product) {
        val cur = (_state.value as? UiState.Success)?.data ?: return
        _state.value = UiState.Success(cur.filter { it.id != p.id })   // optimistic: hide at once
        viewModelScope.launch {
            val r = repo.toggleWishlist(p.id)
            if (r is Result.Error) { _messages.emit(r.message); load() } else _messages.emit((r as Result.Success).message)
        }
    }

    fun moveToCart(p: Product) {
        viewModelScope.launch {
            when (val r = repo.addToCart(p.id)) {
                is Result.Success -> { repo.toggleWishlist(p.id); _messages.emit("Moved to cart."); load() }
                is Result.Error -> _messages.emit(r.message)
            }
        }
    }
}

class WishlistFragment : Fragment() {

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

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val adapter = ProductAdapter(
            onClick = { findNavController().navigate(R.id.productDetailFragment, bundleOf(ProductDetailFragment.ARG_PRODUCT_ID to it.id)) },
            onWishlist = { vm.remove(it) },
            onAddToCart = { vm.moveToCart(it) }
        )
        binding.list.layoutManager = GridLayoutManager(requireContext(), resources.getInteger(R.integer.product_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 -> {
                    adapter.submitList(state.data)
                    if (state.data.isEmpty()) binding.state.empty(R.drawable.ic_heart_outline, R.string.wishlist_empty, R.string.start_shopping) {
                        findNavController().navigate(R.id.homeFragment)
                    } else binding.state.done()
                }
            }
        }
        collectWhenStarted(vm.messages) { snack(it) }
    }

    override fun onResume() {
        super.onResume()
        vm.load()
    }

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

Optimistic updates

When the user removes an item, we hide it immediately and call the API in the background. That feels instant. If the call fails, we show the error and reload, so the item comes back.

Wait for the server

  • tap ♥
  • spinner 300–800 ms
  • item disappears

Optimistic (KartEasy wishlist)

  • tap ♥
  • item disappears at once
  • API runs in background; on error → reload
Use optimistic UI for actions that almost never fail

Move to cart and save for later

From the wishlist, + adds the product to the cart and removes it from the wishlist. From the cart, Save for later does the opposite. The second toggle handles the case where the product was already in the wishlist.

Kotlin – CartViewModel.kt (save for later)
// CartViewModel.kt – "Save for later" moves an item from the cart to the wishlist
fun moveToWishlist(item: CartItem) {
    viewModelScope.launch {
        val r = repo.toggleWishlist(item.productId)
        if (r is Result.Success && !r.data.inWishlist) repo.toggleWishlist(item.productId) // was already there
        update { repo.removeFromCart(item.productId) }
        _messages.emit("Moved to wishlist.")
    }
}

Test it

  1. 1Tap ♡ on Home → Snackbar "Added to wishlist." and the heart fills
  2. 2Open Wishlist tab → the product is there
  3. 3Open the same product → the toolbar heart is filled
  4. 4In Wishlist tap + → "Moved to cart.", the cart badge increases, the item leaves the wishlist
  5. 5In Cart tap Save for later → item moves back to Wishlist
  6. 6Remove the last item → empty state with Start shopping
Wishlist checklist

Frequently Asked Questions

What if the removal fails after the item already disappeared?
The ViewModel shows the error message and calls load() again, so the item comes back. The user sees the truth from the server within a second.
Can guests use the wishlist?
In KartEasy everyone is logged in. For guest wishlists, store product ids in DataStore and send them to wishlist/toggle.php after login.
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

Next topicAddress & Checkout Your progress: 22 / 28 (79%) · Lesson 23 / 28 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free