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

Address & Checkout

The checkout flow

CartCheckout buttonCheckoutaddress + coupon + CODAddress listChange / + AddPlace orderorders/place.phpSuccessnotification + track
From the cart to a placed order
9:41▮▮ 85%←Checkout⋮📍 Deliver toChangeRiya Sharma · 987654…12 MG Road, Indore, MP …3 items in this order🏷 Apply couponCoupon code ApplyWELCOME50 · ₹50 offCash on deliveryTotal amount₹2,597Place order9:41▮▮ 85%←Saved addresses⋮Riya Sharma · 987654…12 MG Road, Indore – 45…Deliver hereEditOffice · 98765000004th Floor, Tech Park, I…+ Add address9:41▮▮ 85%←Add address⋮Full name+91 Mobile numberHouse no, building, streetArea, landmark (optional)City PincodeState ▾Make this my default addressSave address
Checkout, the address picker (pick mode shows "Deliver here"), and the address form

Step 1: AddressViewModel (shared)

Kotlin – ui/address/AddressViewModel.kt
package com.codingeasily.karteasy.ui.address

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.Address
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

/** Shared by the address list and the add/edit form. */
class AddressViewModel(app: Application) : AndroidViewModel(app) {

    private val repo = app.kartApp.repository
    private val _state = MutableStateFlow<UiState<List<Address>>>(UiState.Loading)
    val state: StateFlow<UiState<List<Address>>> = _state

    private val _saved = MutableSharedFlow<Address>()
    val saved: SharedFlow<Address> = _saved
    private val _messages = MutableSharedFlow<String>()
    val messages: SharedFlow<String> = _messages
    private val _saving = MutableStateFlow(false)
    val saving: StateFlow<Boolean> = _saving

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

    fun find(id: Int): Address? = (_state.value as? UiState.Success)?.data?.firstOrNull { it.id == id }

    fun save(a: Address) {
        if (_saving.value) return
        _saving.value = true
        viewModelScope.launch {
            when (val r = repo.saveAddress(a)) {
                is Result.Success -> { _messages.emit(r.message); _saved.emit(r.data) }
                is Result.Error -> _messages.emit(r.message)
            }
            _saving.value = false
        }
    }

    fun delete(a: Address) {
        viewModelScope.launch {
            when (val r = repo.deleteAddress(a.id)) {
                is Result.Success -> { _messages.emit(r.message); load() }
                is Result.Error -> _messages.emit(r.message)
            }
        }
    }
}

Step 2: Address list

The same screen works in two modes. From the Account screen or drawer it is a normal list (tap = edit). From checkout it opens with pick = true and every card gets a Deliver here button.

XML – layout/fragment_address_list.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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.recyclerview.widget.RecyclerView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        android:paddingTop="6dp"
        android:paddingBottom="96dp"
        tools:listitem="@layout/item_address" />

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

    <com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
        android:id="@+id/add_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="bottom|end"
        android:layout_margin="16dp"
        android:text="@string/add_address"
        app:icon="@drawable/ic_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
XML – layout/item_address.xml
<?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"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginStart="12dp"
    android:layout_marginTop="6dp"
    android:layout_marginEnd="12dp"
    android:layout_marginBottom="6dp"
    android:clickable="true"
    android:focusable="true">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

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

            <TextView
                android:id="@+id/name"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:textColor="@color/text_primary"
                android:textStyle="bold"
                tools:text="Riya Sharma · 9876543210" />

            <TextView
                android:id="@+id/default_badge"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@drawable/bg_pill"
                android:backgroundTint="@color/brand_light"
                android:paddingStart="10dp"
                android:paddingTop="2dp"
                android:paddingEnd="10dp"
                android:paddingBottom="2dp"
                android:text="@string/default_label"
                android:textColor="@color/brand"
                android:textSize="12sp"
                android:textStyle="bold" />
        </LinearLayout>

        <TextView
            android:id="@+id/address"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="4dp"
            android:textColor="@color/text_secondary"
            tools:text="12 MG Road, Indore, Madhya Pradesh - 452001" />

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

            <com.google.android.material.button.MaterialButton
                android:id="@+id/deliver_here"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:backgroundTint="@color/accent"
                android:minHeight="40dp"
                android:text="@string/deliver_here"
                android:textColor="@android:color/white"
                android:visibility="gone" />

            <Space
                android:layout_width="0dp"
                android:layout_height="0dp"
                android:layout_weight="1" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/edit"
                style="@style/Widget.Material3.Button.TextButton.Icon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/edit"
                app:icon="@drawable/ic_edit" />

            <com.google.android.material.button.MaterialButton
                android:id="@+id/delete"
                style="@style/Widget.Material3.Button.TextButton.Icon"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/delete"
                android:textColor="@color/sale"
                app:icon="@drawable/ic_delete"
                app:iconTint="@color/sale" />
        </LinearLayout>
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>
Kotlin – ui/address/AddressAdapter.kt
package com.codingeasily.karteasy.ui.address

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.model.Address
import com.codingeasily.karteasy.databinding.ItemAddressBinding
import com.codingeasily.karteasy.util.visibleIf

class AddressAdapter(
    private val pickMode: Boolean,
    private val onPick: (Address) -> Unit,
    private val onEdit: (Address) -> Unit,
    private val onDelete: (Address) -> Unit
) : RecyclerView.Adapter<AddressAdapter.Holder>() {

    var items: List<Address> = emptyList()
        set(value) {
            field = value
            notifyDataSetChanged()
        }

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

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        Holder(ItemAddressBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    override fun getItemCount() = items.size

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val a = items[position]
        val b = holder.b
        b.name.text = b.root.context.getString(R.string.name_phone, a.name, a.phone)
        b.address.text = a.fullText()
        b.defaultBadge.visibleIf(a.isDefault)
        b.deliverHere.visibleIf(pickMode)
        b.deliverHere.setOnClickListener { onPick(a) }
        b.edit.setOnClickListener { onEdit(a) }
        b.delete.setOnClickListener { onDelete(a) }
        b.root.setOnClickListener { if (pickMode) onPick(a) else onEdit(a) }
    }
}
Kotlin – ui/address/AddressListFragment.kt
package com.codingeasily.karteasy.ui.address

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.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.api.UiState
import com.codingeasily.karteasy.data.model.Address
import com.codingeasily.karteasy.databinding.FragmentAddressListBinding
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.google.android.material.dialog.MaterialAlertDialogBuilder

class AddressListFragment : Fragment() {

    companion object {
        /** true when opened from checkout: tapping an address selects it and goes back. */
        const val ARG_PICK = "pick"
        const val RESULT_ADDRESS_ID = "selected_address_id"
    }

    private var _binding: FragmentAddressListBinding? = null
    private val binding get() = _binding!!
    // activityViewModels: the edit screen shares the same ViewModel (and list).
    private val vm: AddressViewModel by activityViewModels()

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val pick = arguments?.getBoolean(ARG_PICK) == true
        val adapter = AddressAdapter(
            pickMode = pick,
            onPick = ::pickAddress,
            onEdit = { openEditor(it.id) },
            onDelete = ::confirmDelete
        )
        binding.list.layoutManager = LinearLayoutManager(requireContext())
        binding.list.adapter = adapter
        binding.addButton.setOnClickListener { openEditor(0) }

        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.items = state.data
                    if (state.data.isEmpty()) binding.state.empty(R.drawable.ic_location, R.string.no_addresses)
                    else binding.state.done()
                }
            }
        }
        collectWhenStarted(vm.messages) { snack(it) }
    }

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

    private fun pickAddress(a: Address) {
        // Send the id to the previous screen (Checkout) and go back.
        findNavController().previousBackStackEntry?.savedStateHandle?.set(RESULT_ADDRESS_ID, a.id)
        findNavController().navigateUp()
    }

    private fun openEditor(id: Int) {
        findNavController().navigate(R.id.addressEditFragment, bundleOf(AddressEditFragment.ARG_ADDRESS_ID to id))
    }

    private fun confirmDelete(a: Address) {
        MaterialAlertDialogBuilder(requireContext())
            .setTitle(R.string.delete_address)
            .setMessage(a.fullText())
            .setNegativeButton(R.string.cancel, null)
            .setPositiveButton(R.string.delete) { _, _ -> vm.delete(a) }
            .show()
    }

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

Step 3: Add / edit address form

The State field is a Material exposed dropdown menu (MaterialAutoCompleteTextView with inputType="none") filled from arrays.xml. Every field is validated before saving, with the error shown under that field.

XML – layout/fragment_address_edit.xml
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:padding="16dp">

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/name_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="@string/full_name">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/name_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="name"
                android:inputType="textPersonName|textCapWords" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/phone_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/mobile"
            app:prefixText="+91 ">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/phone_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="phone"
                android:inputType="phone"
                android:maxLength="10" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/line1_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/address_line1">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/line1_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:autofillHints="postalAddress"
                android:inputType="textPostalAddress|textCapWords" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/line2_layout"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/address_line2">

            <com.google.android.material.textfield.TextInputEditText
                android:id="@+id/line2_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="textPostalAddress|textCapWords" />
        </com.google.android.material.textfield.TextInputLayout>

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

            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/city_layout"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginEnd="6dp"
                android:layout_weight="1"
                android:hint="@string/city">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/city_input"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:inputType="textCapWords" />
            </com.google.android.material.textfield.TextInputLayout>

            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/pincode_layout"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="6dp"
                android:layout_weight="1"
                android:hint="@string/pincode">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/pincode_input"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:autofillHints="postalCode"
                    android:inputType="number"
                    android:maxLength="6" />
            </com.google.android.material.textfield.TextInputLayout>
        </LinearLayout>

        <!-- Exposed dropdown menu = Material spinner -->
        <com.google.android.material.textfield.TextInputLayout
            android:id="@+id/state_layout"
            style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:hint="@string/state">

            <com.google.android.material.textfield.MaterialAutoCompleteTextView
                android:id="@+id/state_input"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="none" />
        </com.google.android.material.textfield.TextInputLayout>

        <com.google.android.material.materialswitch.MaterialSwitch
            android:id="@+id/default_switch"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="12dp"
            android:text="@string/make_default" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/save_button"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="24dp"
            android:text="@string/save_address" />
    </LinearLayout>
</ScrollView>
XML – values/arrays.xml (all states and union territories)
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="indian_states">
        <item>Andaman and Nicobar Islands</item>
        <item>Andhra Pradesh</item>
        <item>Arunachal Pradesh</item>
        <item>Assam</item>
        <item>Bihar</item>
        <item>Chandigarh</item>
        <item>Chhattisgarh</item>
        <item>Dadra and Nagar Haveli and Daman and Diu</item>
        <item>Delhi</item>
        <item>Goa</item>
        <item>Gujarat</item>
        <item>Haryana</item>
        <item>Himachal Pradesh</item>
        <item>Jammu and Kashmir</item>
        <item>Jharkhand</item>
        <item>Karnataka</item>
        <item>Kerala</item>
        <item>Ladakh</item>
        <item>Lakshadweep</item>
        <item>Madhya Pradesh</item>
        <item>Maharashtra</item>
        <item>Manipur</item>
        <item>Meghalaya</item>
        <item>Mizoram</item>
        <item>Nagaland</item>
        <item>Odisha</item>
        <item>Puducherry</item>
        <item>Punjab</item>
        <item>Rajasthan</item>
        <item>Sikkim</item>
        <item>Tamil Nadu</item>
        <item>Telangana</item>
        <item>Tripura</item>
        <item>Uttar Pradesh</item>
        <item>Uttarakhand</item>
        <item>West Bengal</item>
    </string-array>
</resources>
Kotlin – ui/address/AddressEditFragment.kt
package com.codingeasily.karteasy.ui.address

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.model.Address
import com.codingeasily.karteasy.databinding.FragmentAddressEditBinding
import com.codingeasily.karteasy.util.Validator
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.snack
import com.google.android.material.textfield.TextInputLayout

class AddressEditFragment : Fragment() {

    companion object {
        const val ARG_ADDRESS_ID = "addressId"
    }

    private var _binding: FragmentAddressEditBinding? = null
    private val binding get() = _binding!!
    private val vm: AddressViewModel by activityViewModels()
    private var editingId = 0

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        editingId = arguments?.getInt(ARG_ADDRESS_ID) ?: 0
        (requireActivity() as AppCompatActivity).supportActionBar?.setTitle(
            if (editingId > 0) R.string.edit_address else R.string.add_address
        )

        // State dropdown (exposed dropdown menu)
        val states = resources.getStringArray(R.array.indian_states)
        binding.stateInput.setAdapter(ArrayAdapter(requireContext(), android.R.layout.simple_list_item_1, states))

        if (savedInstanceState == null) vm.find(editingId)?.let { fill(it) }

        binding.saveButton.setOnClickListener { save() }
        collectWhenStarted(vm.saving) { binding.saveButton.isEnabled = !it }
        collectWhenStarted(vm.saved) { findNavController().navigateUp() }
        collectWhenStarted(vm.messages) { snack(it) }
    }

    private fun fill(a: Address) {
        binding.nameInput.setText(a.name)
        binding.phoneInput.setText(a.phone)
        binding.line1Input.setText(a.line1)
        binding.line2Input.setText(a.line2)
        binding.cityInput.setText(a.city)
        binding.stateInput.setText(a.state, false)
        binding.pincodeInput.setText(a.pincode)
        binding.defaultSwitch.isChecked = a.isDefault
    }

    private fun save() {
        val name = binding.nameInput.text.toString()
        val phone = binding.phoneInput.text.toString()
        val line1 = binding.line1Input.text.toString()
        val city = binding.cityInput.text.toString()
        val state = binding.stateInput.text.toString()
        val pin = binding.pincodeInput.text.toString()

        fun check(layout: TextInputLayout, ok: Boolean, msg: Int): Boolean {
            layout.error = if (ok) null else getString(msg)
            return ok
        }
        val valid = listOf(
            check(binding.nameLayout, Validator.name(name), R.string.err_name),
            check(binding.phoneLayout, Validator.phone(phone), R.string.err_phone),
            check(binding.line1Layout, line1.trim().length >= 5, R.string.err_address),
            check(binding.cityLayout, city.isNotBlank(), R.string.err_required),
            check(binding.stateLayout, state.isNotBlank(), R.string.err_required),
            check(binding.pincodeLayout, Validator.pincode(pin), R.string.err_pincode)
        ).all { it }
        if (!valid) return

        vm.save(
            Address(
                id = editingId, name = name, phone = phone, line1 = line1,
                line2 = binding.line2Input.text.toString(), city = city, state = state,
                pincode = pin, isDefault = binding.defaultSwitch.isChecked
            )
        )
    }

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

Step 4: Send the chosen address back

Navigation has a simple way to return a value to the previous screen: write it into the previous back-stack entry's SavedStateHandle, then go back. Checkout observes that key.

CheckoutFragmentAddressListFragmentnavigate(addressList, pick = true)user taps "Deliver here"previousBackStackEntry.savedStateHandle["selected_address_id"] = 2getLiveData observer → selectAddress(2)
Returning a result with SavedStateHandle

Step 5: Checkout layout

Four cards — address, coupon, payment, price — and the bottom bar with the total and Place order. When a coupon is applied the input row is replaced by a green "SAVE10 applied" row with Remove.

XML – layout/fragment_checkout.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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.core.widget.NestedScrollView
        android:id="@+id/content"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toTopOf="@id/bottom_bar"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:paddingTop="4dp">

            <!-- 1. Delivery address -->
            <com.google.android.material.card.MaterialCardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="12dp">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:padding="16dp">

                    <TextView
                        style="@style/KartEasy.CardTitle"
                        android:drawablePadding="8dp"
                        android:text="@string/deliver_to"
                        app:drawableStartCompat="@drawable/ic_location"
                        app:drawableTint="@color/brand" />

                    <LinearLayout
                        android:id="@+id/address_card"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:gravity="center_vertical"
                        android:orientation="horizontal">

                        <LinearLayout
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_weight="1"
                            android:orientation="vertical">

                            <TextView
                                android:id="@+id/address_name"
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:textColor="@color/text_primary"
                                android:textStyle="bold"
                                tools:text="Riya Sharma · 9876543210" />

                            <TextView
                                android:id="@+id/address_text"
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:layout_marginTop="2dp"
                                android:textColor="@color/text_secondary"
                                tools:text="12 MG Road, Indore, Madhya Pradesh - 452001" />
                        </LinearLayout>

                        <com.google.android.material.button.MaterialButton
                            android:id="@+id/change_address"
                            style="@style/Widget.Material3.Button.OutlinedButton"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_marginStart="8dp"
                            android:text="@string/change" />
                    </LinearLayout>

                    <com.google.android.material.button.MaterialButton
                        android:id="@+id/add_address"
                        style="@style/Widget.KartEasy.Button.Outlined"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:text="@string/add_delivery_address"
                        android:visibility="gone" />
                </LinearLayout>
            </com.google.android.material.card.MaterialCardView>

            <TextView
                android:id="@+id/items_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingStart="16dp"
                android:paddingEnd="16dp"
                android:textColor="@color/text_secondary"
                tools:text="3 items in this order" />

            <!-- 2. Coupon -->
            <com.google.android.material.card.MaterialCardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_margin="12dp">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:padding="16dp">

                    <TextView
                        style="@style/KartEasy.CardTitle"
                        android:drawablePadding="8dp"
                        android:text="@string/apply_coupon"
                        app:drawableStartCompat="@drawable/ic_offer"
                        app:drawableTint="@color/accent" />

                    <LinearLayout
                        android:id="@+id/coupon_form"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:orientation="horizontal">

                        <com.google.android.material.textfield.TextInputLayout
                            android:id="@+id/coupon_layout"
                            style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_weight="1"
                            android:hint="@string/coupon_code">

                            <com.google.android.material.textfield.TextInputEditText
                                android:id="@+id/coupon_input"
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:inputType="textCapCharacters"
                                android:maxLength="30"
                                android:maxLines="1" />
                        </com.google.android.material.textfield.TextInputLayout>

                        <com.google.android.material.button.MaterialButton
                            android:id="@+id/apply_coupon"
                            style="@style/Widget.Material3.Button.TonalButton"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_marginStart="8dp"
                            android:layout_marginTop="4dp"
                            android:text="@string/apply" />
                    </LinearLayout>

                    <com.google.android.material.chip.ChipGroup
                        android:id="@+id/coupon_chips"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:layout_marginTop="8dp" />

                    <LinearLayout
                        android:id="@+id/applied_coupon"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:background="@drawable/bg_offer_card"
                        android:gravity="center_vertical"
                        android:orientation="horizontal"
                        android:padding="12dp"
                        android:visibility="gone">

                        <TextView
                            android:id="@+id/applied_coupon_text"
                            android:layout_width="0dp"
                            android:layout_height="wrap_content"
                            android:layout_weight="1"
                            android:textColor="@color/success"
                            android:textStyle="bold"
                            tools:text="SAVE10 applied · you save ₹259.70" />

                        <com.google.android.material.button.MaterialButton
                            android:id="@+id/remove_coupon"
                            style="@style/Widget.Material3.Button.TextButton"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:text="@string/remove"
                            android:textColor="@color/sale" />
                    </LinearLayout>
                </LinearLayout>
            </com.google.android.material.card.MaterialCardView>

            <!-- 3. Payment -->
            <com.google.android.material.card.MaterialCardView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginStart="12dp"
                android:layout_marginEnd="12dp">

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="vertical"
                    android:padding="16dp">

                    <TextView
                        style="@style/KartEasy.CardTitle"
                        android:text="@string/payment_method" />

                    <com.google.android.material.radiobutton.MaterialRadioButton
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:checked="true"
                        android:text="@string/cash_on_delivery" />

                    <TextView
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:paddingStart="32dp"
                        android:text="@string/cod_note"
                        android:textColor="@color/text_secondary"
                        android:textSize="13sp" />
                </LinearLayout>
            </com.google.android.material.card.MaterialCardView>

            <!-- 4. Price -->
            <include
                android:id="@+id/summary"
                layout="@layout/layout_price_summary" />
        </LinearLayout>
    </androidx.core.widget.NestedScrollView>

    <LinearLayout
        android:id="@+id/bottom_bar"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:background="@drawable/bg_bottom_bar"
        android:elevation="8dp"
        android:gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="12dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <LinearLayout
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical">

            <TextView
                android:id="@+id/total"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textColor="@color/text_primary"
                android:textSize="20sp"
                android:textStyle="bold"
                tools:text="₹2,337" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/total_amount"
                android:textColor="@color/text_secondary"
                android:textSize="12sp" />
        </LinearLayout>

        <com.google.android.material.progressindicator.CircularProgressIndicator
            android:id="@+id/placing_progress"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginEnd="8dp"
            android:indeterminate="true"
            android:visibility="gone"
            app:indicatorSize="24dp" />

        <com.google.android.material.button.MaterialButton
            android:id="@+id/place_order"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:backgroundTint="@color/accent"
            android:paddingStart="28dp"
            android:paddingEnd="28dp"
            android:text="@string/place_order"
            android:textColor="@android:color/white" />
    </LinearLayout>

    <include
        android:id="@+id/state"
        layout="@layout/layout_state" />
</androidx.constraintlayout.widget.ConstraintLayout>

Step 6: CheckoutViewModel

On start, the cart, addresses and coupons are loaded in parallel with async — three requests in the time of one. The default address is pre-selected. placeOrder() blocks double taps with placing.

Kotlin – ui/checkout/CheckoutViewModel.kt
package com.codingeasily.karteasy.ui.checkout

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.codingeasily.karteasy.data.api.Result
import com.codingeasily.karteasy.data.model.Address
import com.codingeasily.karteasy.data.model.Cart
import com.codingeasily.karteasy.data.model.Coupon
import com.codingeasily.karteasy.data.model.PlacedOrder
import com.codingeasily.karteasy.util.kartApp
import kotlinx.coroutines.async
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 CheckoutViewModel(app: Application) : AndroidViewModel(app) {

    data class State(
        val loading: Boolean = true,
        val error: String? = null,
        val cart: Cart? = null,
        val addresses: List<Address> = emptyList(),
        val selectedAddressId: Int = 0,
        val coupons: List<Coupon> = emptyList(),
        val appliedCoupon: String = "",
        val placing: Boolean = false
    ) {
        val selectedAddress: Address? get() = addresses.firstOrNull { it.id == selectedAddressId }
    }

    sealed class Event {
        data class Message(val text: String) : Event()
        data class CouponError(val text: String) : Event()
        data class OrderPlaced(val order: PlacedOrder) : Event()
    }

    private val repo = app.kartApp.repository
    private val _state = MutableStateFlow(State())
    val state: StateFlow<State> = _state
    private val _events = MutableSharedFlow<Event>()
    val events: SharedFlow<Event> = _events

    init { load() }

    /** Cart, addresses and coupons are loaded in parallel with async. */
    fun load() {
        _state.value = _state.value.copy(loading = true, error = null)
        viewModelScope.launch {
            val cartJob = async { repo.cart(_state.value.appliedCoupon.ifEmpty { null }) }
            val addrJob = async { repo.addresses() }
            val couponJob = async { repo.coupons() }
            val cart = cartJob.await()
            val addresses = addrJob.await()
            val coupons = couponJob.await()

            if (cart is Result.Error) {
                _state.value = _state.value.copy(loading = false, error = cart.message); return@launch
            }
            if (addresses is Result.Error) {
                _state.value = _state.value.copy(loading = false, error = addresses.message); return@launch
            }
            val list = (addresses as Result.Success).data
            val keep = _state.value.selectedAddressId
            val selected = list.firstOrNull { it.id == keep } ?: list.firstOrNull { it.isDefault } ?: list.firstOrNull()
            _state.value = _state.value.copy(
                loading = false,
                cart = (cart as Result.Success).data,
                addresses = list,
                selectedAddressId = selected?.id ?: 0,
                coupons = (coupons as? Result.Success)?.data ?: emptyList()
            )
        }
    }

    fun selectAddress(id: Int) {
        _state.value = _state.value.copy(selectedAddressId = id)
    }

    fun applyCoupon(code: String) {
        if (code.isBlank()) return
        viewModelScope.launch {
            when (val r = repo.applyCoupon(code)) {
                is Result.Success -> {
                    _state.value = _state.value.copy(cart = r.data, appliedCoupon = r.data.couponCode ?: "")
                    _events.emit(Event.Message(r.message))
                }
                is Result.Error -> _events.emit(Event.CouponError(r.message))
            }
        }
    }

    fun removeCoupon() {
        viewModelScope.launch {
            val r = repo.cart()
            if (r is Result.Success) _state.value = _state.value.copy(cart = r.data, appliedCoupon = "")
        }
    }

    fun placeOrder() {
        val s = _state.value
        if (s.placing) return
        val address = s.selectedAddress
        if (address == null) {
            viewModelScope.launch { _events.emit(Event.Message("Please add a delivery address.")) }
            return
        }
        _state.value = s.copy(placing = true)
        viewModelScope.launch {
            when (val r = repo.placeOrder(address.id, s.appliedCoupon)) {
                is Result.Success -> _events.emit(Event.OrderPlaced(r.data))
                is Result.Error -> {
                    _events.emit(Event.Message(r.message))
                    load()  // stock or prices may have changed: show the latest cart
                }
            }
            _state.value = _state.value.copy(placing = false)
        }
    }
}
CheckoutViewModelAPIasync cart/list.phpasync address/list.phpasync coupons/list.phpthree answers (≈ one round trip)select default address, build state
Parallel loading with async / await

Step 7: CheckoutFragment

Coupon chips are created in code from the API list — tapping one fills the code and applies it. A coupon error is shown inside the coupon field (couponLayout.error), other messages as a Snackbar. After a successful order we show the notification and open the success screen.

Kotlin – ui/checkout/CheckoutFragment.kt
package com.codingeasily.karteasy.ui.checkout

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.navigation.fragment.findNavController
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.databinding.FragmentCheckoutBinding
import com.codingeasily.karteasy.notify.Notifier
import com.codingeasily.karteasy.ui.address.AddressListFragment
import com.codingeasily.karteasy.ui.cart.bind
import com.codingeasily.karteasy.ui.orders.OrderSuccessFragment
import com.codingeasily.karteasy.util.Format
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.codingeasily.karteasy.util.visibleIf
import com.google.android.material.chip.Chip

class CheckoutFragment : Fragment() {

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

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

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        // The address list screen sends the chosen address back through our SavedStateHandle.
        val handle = findNavController().getBackStackEntry(R.id.checkoutFragment).savedStateHandle
        handle.getLiveData<Int>(AddressListFragment.RESULT_ADDRESS_ID).observe(viewLifecycleOwner) { id ->
            vm.load()
            vm.selectAddress(id)
            handle.remove<Int>(AddressListFragment.RESULT_ADDRESS_ID)
        }

        binding.changeAddress.setOnClickListener {
            findNavController().navigate(R.id.addressListFragment, bundleOf(AddressListFragment.ARG_PICK to true))
        }
        binding.addAddress.setOnClickListener {
            findNavController().navigate(R.id.addressListFragment, bundleOf(AddressListFragment.ARG_PICK to true))
        }
        binding.applyCoupon.setOnClickListener {
            binding.couponLayout.error = null
            vm.applyCoupon(binding.couponInput.text.toString())
        }
        binding.removeCoupon.setOnClickListener {
            binding.couponInput.setText("")
            vm.removeCoupon()
        }
        binding.placeOrder.setOnClickListener { vm.placeOrder() }

        collectWhenStarted(vm.state) { s ->
            when {
                s.loading && s.cart == null -> { binding.content.hide(); binding.bottomBar.hide(); binding.state.loading() }
                s.error != null && s.cart == null -> {
                    binding.content.hide(); binding.bottomBar.hide(); binding.state.error(s.error) { vm.load() }
                }
                else -> {
                    binding.state.done(); binding.content.show(); binding.bottomBar.show()
                    render(s)
                }
            }
        }
        collectWhenStarted(vm.events) { e ->
            when (e) {
                is CheckoutViewModel.Event.Message -> snack(e.text)
                is CheckoutViewModel.Event.CouponError -> binding.couponLayout.error = e.text
                is CheckoutViewModel.Event.OrderPlaced -> {
                    val total = Format.price(e.order.total)
                    Notifier.orderPlaced(requireContext(), e.order.orderId, e.order.orderNo, total)
                    findNavController().navigate(
                        R.id.action_checkout_to_success,
                        bundleOf(
                            OrderSuccessFragment.ARG_ORDER_ID to e.order.orderId,
                            OrderSuccessFragment.ARG_ORDER_NO to e.order.orderNo,
                            OrderSuccessFragment.ARG_TOTAL to total
                        )
                    )
                }
            }
        }
    }

    private fun render(s: CheckoutViewModel.State) {
        val cart = s.cart ?: return
        val a = s.selectedAddress
        binding.addressCard.visibleIf(a != null)
        binding.addAddress.visibleIf(a == null)
        if (a != null) {
            binding.addressName.text = getString(R.string.name_phone, a.name, a.phone)
            binding.addressText.text = a.fullText()
        }

        binding.itemsText.text = resources.getQuantityString(R.plurals.items_in_order, cart.itemCount, cart.itemCount)
        binding.summary.bind(cart)
        binding.total.text = Format.price(cart.total)

        // Applied coupon row vs. coupon input
        val applied = s.appliedCoupon.isNotEmpty()
        binding.appliedCoupon.visibleIf(applied)
        binding.couponForm.visibleIf(!applied)
        binding.appliedCouponText.text = getString(R.string.coupon_applied, s.appliedCoupon, Format.price(cart.discount))

        // Available coupons as chips: tap to fill the code.
        binding.couponChips.removeAllViews()
        s.coupons.forEach { c ->
            val chip = Chip(requireContext()).apply {
                text = getString(R.string.coupon_chip, c.code, c.title)
                isCheckable = false
                setOnClickListener {
                    binding.couponInput.setText(c.code)
                    binding.couponLayout.error = null
                    vm.applyCoupon(c.code)
                }
            }
            binding.couponChips.addView(chip)
        }
        binding.couponChips.visibleIf(!applied && s.coupons.isNotEmpty())

        binding.placeOrder.isEnabled = !s.placing && a != null && cart.itemCount > 0
        binding.placeOrder.setText(if (s.placing) R.string.placing_order else R.string.place_order)
        binding.placingProgress.visibleIf(s.placing)
    }

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

Adding UPI / cards later

Kotlin – comment: where a gateway plugs in
// Where an online payment would go (KartEasy ships with Cash on Delivery only):
// 1. placeOrder() creates the order on the server with status "pending_payment"
// 2. open the payment gateway's Android SDK checkout with order_no + total
// 3. the gateway calls YOUR server (webhook) – the server verifies the signature
//    and marks the order as paid
// 4. the app refreshes the order: status "placed" + paid
// Never mark an order as paid from the app itself.

The server side of this is described in the Orders API lesson.

Frequently Asked Questions

Why does checkout reload after an order error?
Errors at checkout usually mean the cart changed: stock ran out or a coupon expired. Reloading shows the latest cart and totals so the user can decide again.
Why activityViewModels() for addresses?
The list and the edit form share one AddressViewModel scoped to the activity. The form can read the address being edited from the already-loaded list, and saving updates the list without another request from the form.
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

Next topicMy Orders & Order Details Your progress: 23 / 28 (82%) · Lesson 24 / 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