Free tutorials & notes in Hindi & English · Clean code examples 🧰 मुफ़्त PDF और फ़ोटो टूल्स →
📱 Android App (Kotlin + XML)  ·  Lesson 23

Address और Checkout

Checkout flow

CartCheckout buttonCheckoutaddress + coupon + CODAddress listChange / + AddPlace orderorders/place.phpSuccessnotification + track
Cart से 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, address picker (pick mode में "Deliver here"), और 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

यही screen दो modes में चलती है। Account screen या drawer से यह normal list है (tap = edit)। Checkout से यह pick = true के साथ खुलती है और हर card पर 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: Address add / edit form

State field एक Material exposed dropdown menu है (inputType="none" वाला MaterialAutoCompleteTextView) जो arrays.xml से भरता है। Save से पहले हर field validate होता है और error उसी 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 (सारे states और 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: चुना address वापस भेजना

पिछली screen को value लौटाने का Navigation में आसान तरीका है: उसे previous back-stack entry के SavedStateHandle में लिखें, फिर वापस जाएँ। Checkout उस key को observe करता है।

CheckoutFragmentAddressListFragmentnavigate(addressList, pick = true)user "Deliver here" tap करता हैpreviousBackStackEntry.savedStateHandle["selected_address_id"] = 2getLiveData observer → selectAddress(2)
SavedStateHandle से result लौटाना

Step 5: Checkout layout

चार cards — address, coupon, payment, price — और total व Place order वाला bottom bar। Coupon लगने पर input row की जगह Remove के साथ green "SAVE10 applied" row आ जाती है।

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

शुरुआत में cart, addresses और coupons async से parallel में load होते हैं — एक के समय में तीन requests। Default address pre-select होता है। placeOrder() placing से double taps रोकता है।

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.phpतीन जवाब (≈ एक round trip)default address select, state बनाएँ
async / await से parallel loading

Step 7: CheckoutFragment

Coupon chips API list से code में बनते हैं — एक tap code भरकर apply कर देता है। Coupon error coupon field के अंदर दिखता है (couponLayout.error), बाकी messages Snackbar में। Order सफल होने पर notification दिखाकर 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
    }
}

बाद में UPI / cards जोड़ना

Kotlin – comment: gateway कहाँ जुड़ता है
// 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.

इसका server side Orders API lesson में बताया गया है।

अक्सर पूछे जाने वाले प्रश्न (FAQ)

Order error के बाद checkout reload क्यों?
Checkout पर errors का मतलब ज़्यादातर cart बदल गया है: stock ख़त्म या coupon expire। Reload से latest cart और totals दिखते हैं ताकि user फिर से तय करे।
Addresses के लिए activityViewModels() क्यों?
List और edit form activity-scoped एक AddressViewModel share करते हैं। Form पहले से load list से edit होने वाला address पढ़ सकता है, और save करने पर list update हो जाती है।
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

अगला topicMy Orders और Order Details आपकी प्रगति: 23 / 28 (82%) · Lesson 24 / 28 आगे बढ़ें →

यह lesson कैसा लगा?

आपकी राय से हम content और बेहतर बनाते हैं।

अपना review और सुझाव लिखें (वैकल्पिक)

Review approve होने के बाद आपका नाम और शहर इसके साथ public दिख सकते हैं। Email या phone न लिखें।

▶ CodingEasily on YouTube

Video से सीखें – आसान Hindi में

चुनी हुई video: RDBMS भाग 3 – Class XI Commerce IT

▶ Channel subscribe करें