Profile, Drawer और Bottom Nav
App का नक्शा
MainActivity (toolbar + drawer + bottom nav + NavHost)
Bottom tabs (top level, hamburger icon)
homeFragment ─ categoriesFragment ─ wishlistFragment ─ cartFragment ─ accountFragment
गहरी screens (back arrow, bottom bar छुपा)
productListFragment → productDetailFragment
checkoutFragment → addressListFragment → addressEditFragment
orderSuccessFragment → orderDetailFragment
ordersFragment, editProfileFragment
AuthActivity: loginFragment ⇄ registerFragment
OnboardingActivityStep 1: nav_main.xml
सारी shop screens और उनके arguments एक graph में। Optional arguments के defaults हैं, इसलिए navigate(R.id.productListFragment) बिना bundle के भी चलता है। Product detail एक deep link भी declare करता है।
<?xml version="1.0" encoding="utf-8"?>
<!-- Every screen of the shop. Arguments are passed with bundleOf(...) using the ARG_ constants. -->
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_main"
app:startDestination="@id/homeFragment">
<!-- ===== Bottom navigation tabs ===== -->
<fragment
android:id="@+id/homeFragment"
android:name="com.codingeasily.karteasy.ui.home.HomeFragment"
android:label="@string/app_name" />
<fragment
android:id="@+id/categoriesFragment"
android:name="com.codingeasily.karteasy.ui.category.CategoriesFragment"
android:label="@string/nav_categories" />
<fragment
android:id="@+id/wishlistFragment"
android:name="com.codingeasily.karteasy.ui.wishlist.WishlistFragment"
android:label="@string/nav_wishlist" />
<fragment
android:id="@+id/cartFragment"
android:name="com.codingeasily.karteasy.ui.cart.CartFragment"
android:label="@string/nav_cart">
<action
android:id="@+id/action_cart_to_checkout"
app:destination="@id/checkoutFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
</fragment>
<fragment
android:id="@+id/accountFragment"
android:name="com.codingeasily.karteasy.ui.account.AccountFragment"
android:label="@string/nav_account" />
<!-- ===== Catalog ===== -->
<fragment
android:id="@+id/productListFragment"
android:name="com.codingeasily.karteasy.ui.product.ProductListFragment"
android:label="@string/title_products">
<argument
android:name="categoryId"
android:defaultValue="0"
app:argType="integer" />
<argument
android:name="title"
android:defaultValue="@null"
app:argType="string"
app:nullable="true" />
<argument
android:name="sort"
android:defaultValue="newest"
app:argType="string" />
<argument
android:name="focusSearch"
android:defaultValue="false"
app:argType="boolean" />
</fragment>
<fragment
android:id="@+id/productDetailFragment"
android:name="com.codingeasily.karteasy.ui.product.ProductDetailFragment"
android:label="@string/title_product">
<argument
android:name="productId"
app:argType="integer" />
<!-- Opens from links like https://karteasy.in/product/7 (see the deep links lesson) -->
<deepLink app:uri="karteasy.in/product/{productId}" />
</fragment>
<!-- ===== Checkout flow ===== -->
<fragment
android:id="@+id/checkoutFragment"
android:name="com.codingeasily.karteasy.ui.checkout.CheckoutFragment"
android:label="@string/title_checkout">
<action
android:id="@+id/action_checkout_to_success"
app:destination="@id/orderSuccessFragment"
app:popUpTo="@id/cartFragment" />
</fragment>
<fragment
android:id="@+id/orderSuccessFragment"
android:name="com.codingeasily.karteasy.ui.orders.OrderSuccessFragment"
android:label="@string/title_order_placed">
<argument android:name="orderId" app:argType="integer" />
<argument android:name="orderNo" app:argType="string" />
<argument android:name="total" app:argType="string" />
<action
android:id="@+id/action_success_to_order_detail"
app:destination="@id/orderDetailFragment"
app:popUpTo="@id/homeFragment" />
</fragment>
<fragment
android:id="@+id/addressListFragment"
android:name="com.codingeasily.karteasy.ui.address.AddressListFragment"
android:label="@string/nav_addresses">
<argument
android:name="pick"
android:defaultValue="false"
app:argType="boolean" />
</fragment>
<fragment
android:id="@+id/addressEditFragment"
android:name="com.codingeasily.karteasy.ui.address.AddressEditFragment"
android:label="@string/title_address_edit">
<argument
android:name="addressId"
android:defaultValue="0"
app:argType="integer" />
</fragment>
<!-- ===== Orders & account ===== -->
<fragment
android:id="@+id/ordersFragment"
android:name="com.codingeasily.karteasy.ui.orders.OrdersFragment"
android:label="@string/nav_orders" />
<fragment
android:id="@+id/orderDetailFragment"
android:name="com.codingeasily.karteasy.ui.orders.OrderDetailFragment"
android:label="@string/title_order_detail">
<argument
android:name="orderId"
app:argType="integer" />
</fragment>
<fragment
android:id="@+id/editProfileFragment"
android:name="com.codingeasily.karteasy.ui.account.EditProfileFragment"
android:label="@string/title_edit_profile" />
</navigation>Step 2: activity_main.xml
DrawerLayout के दो children हैं: main column (toolbar, offline banner, NavHost, bottom navigation) और header वाला NavigationView drawer।
<?xml version="1.0" encoding="utf-8"?>
<!-- DrawerLayout = side menu. Inside: app bar, offline banner, screens (NavHost) and bottom navigation. -->
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground"
android:orientation="vertical">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/brand"
android:elevation="4dp"
app:navigationIconTint="@color/on_brand"
app:titleTextColor="@color/on_brand" />
<TextView
android:id="@+id/offline_banner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/offline"
android:drawablePadding="8dp"
android:gravity="center"
android:padding="6dp"
android:text="@string/offline_banner"
android:textColor="@android:color/white"
android:textSize="13sp"
android:visibility="gone" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_main" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/surface"
app:itemActiveIndicatorStyle="@style/Widget.Material3.BottomNavigationView.ActiveIndicator"
app:itemIconTint="@color/nav_item_color"
app:itemTextColor="@color/nav_item_color"
app:labelVisibilityMode="labeled"
app:menu="@menu/bottom_nav_menu" />
</LinearLayout>
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/nav_header"
app:menu="@menu/drawer_menu" />
</androidx.drawerlayout.widget.DrawerLayout><?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_header_gradient"
android:orientation="vertical"
android:paddingStart="20dp"
android:paddingTop="32dp"
android:paddingEnd="20dp"
android:paddingBottom="20dp">
<TextView
android:id="@+id/header_avatar"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@drawable/bg_avatar"
android:gravity="center"
android:text="K"
android:textColor="@android:color/white"
android:textSize="28sp"
android:textStyle="bold" />
<TextView
android:id="@+id/header_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="@android:color/white"
android:textStyle="bold" />
<TextView
android:id="@+id/header_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#DDFFFFFF" />
</LinearLayout>Step 3: bottom और drawer menus
Screen items destinations वाले ही ids use करते हैं। जो items actions हैं (share, help, logout) वे action_… ids use करते हैं और code में handle होते हैं।
<?xml version="1.0" encoding="utf-8"?>
<!-- Item ids MUST match the destination ids in nav_main.xml so setupWithNavController() works -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/homeFragment" android:icon="@drawable/ic_home" android:title="@string/nav_home" />
<item android:id="@+id/categoriesFragment" android:icon="@drawable/ic_category" android:title="@string/nav_categories" />
<item android:id="@+id/wishlistFragment" android:icon="@drawable/ic_heart_outline" android:title="@string/nav_wishlist" />
<item android:id="@+id/cartFragment" android:icon="@drawable/ic_cart" android:title="@string/nav_cart" />
<item android:id="@+id/accountFragment" android:icon="@drawable/ic_person" android:title="@string/nav_account" />
</menu><?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Screens: ids match nav_main.xml destinations -->
<group android:id="@+id/group_main" android:checkableBehavior="single">
<item android:id="@+id/homeFragment" android:icon="@drawable/ic_home" android:title="@string/nav_home" />
<item android:id="@+id/categoriesFragment" android:icon="@drawable/ic_category" android:title="@string/nav_categories" />
<item android:id="@+id/ordersFragment" android:icon="@drawable/ic_orders" android:title="@string/nav_orders" />
<item android:id="@+id/wishlistFragment" android:icon="@drawable/ic_heart_outline" android:title="@string/nav_wishlist" />
<item android:id="@+id/addressListFragment" android:icon="@drawable/ic_location" android:title="@string/nav_addresses" />
<item android:id="@+id/accountFragment" android:icon="@drawable/ic_person" android:title="@string/nav_account" />
</group>
<!-- Actions handled in MainActivity -->
<item android:title="@string/drawer_section_more">
<menu>
<item android:id="@+id/action_share" android:icon="@drawable/ic_share" android:title="@string/share_app" />
<item android:id="@+id/action_help" android:icon="@drawable/ic_help" android:title="@string/help" />
<item android:id="@+id/action_logout" android:icon="@drawable/ic_logout" android:title="@string/logout" />
</menu>
</item>
</menu>Step 4: MainActivity
पूरी activity: splash decision (Splash lesson से), पाँच top-level destinations वाला AppBarConfiguration, NavController से जुड़े bottom nav + drawer, bottom bar छुपाने वाला destination listener, Back जो पहले drawer बंद करे, live drawer header, cart badge, offline banner, और expire sessions को login पर भेजने वाला 401 check।
package com.codingeasily.karteasy
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.GravityCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.codingeasily.karteasy.databinding.ActivityMainBinding
import com.codingeasily.karteasy.ui.auth.AuthActivity
import com.codingeasily.karteasy.ui.onboarding.OnboardingActivity
import com.codingeasily.karteasy.util.hide
import com.codingeasily.karteasy.util.kartApp
import com.codingeasily.karteasy.util.visibleIf
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
private lateinit var appBarConfig: AppBarConfiguration
private var ready = false
private val askNotifications = registerForActivityResult(ActivityResultContracts.RequestPermission()) { }
override fun onCreate(savedInstanceState: Bundle?) {
// 1) Splash screen stays until we know where to go (onboarding / login / home).
val splash = installSplashScreen()
splash.setKeepOnScreenCondition { !ready }
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val app = kartApp
app.session.load()
applyDarkMode(app.session.darkMode())
when {
!app.session.isOnboardingDone() -> openAndFinish(OnboardingActivity::class.java)
!app.session.isLoggedIn -> openAndFinish(AuthActivity::class.java)
else -> setupUi()
}
ready = true
}
}
private fun openAndFinish(target: Class<*>) {
startActivity(Intent(this, target))
finish()
}
private fun setupUi() {
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
val host = supportFragmentManager.findFragmentById(R.id.nav_host) as NavHostFragment
navController = host.navController
// Top-level screens show the hamburger icon instead of a back arrow.
appBarConfig = AppBarConfiguration(
setOf(R.id.homeFragment, R.id.categoriesFragment, R.id.wishlistFragment, R.id.cartFragment, R.id.accountFragment),
binding.drawerLayout
)
setupActionBarWithNavController(navController, appBarConfig)
binding.bottomNav.setupWithNavController(navController)
binding.navView.setupWithNavController(navController)
// Drawer items that are not screens
binding.navView.setNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.action_share -> shareApp()
R.id.action_help -> showHelp()
R.id.action_logout -> confirmLogout()
else -> NavigationUI.onNavDestinationSelected(item, navController)
}
binding.drawerLayout.closeDrawer(GravityCompat.START)
true
}
// Hide the bottom bar on detail / checkout screens.
navController.addOnDestinationChangedListener { _, destination, _ ->
val top = destination.id in appBarConfig.topLevelDestinations
binding.bottomNav.visibleIf(top)
}
// Back button closes the drawer first.
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (binding.drawerLayout.isDrawerOpen(GravityCompat.START)) {
binding.drawerLayout.closeDrawer(GravityCompat.START)
} else {
isEnabled = false
onBackPressedDispatcher.onBackPressed()
isEnabled = true
}
}
})
observeApp()
askNotificationPermission()
}
private fun observeApp() {
val app = kartApp
val header = binding.navView.getHeaderView(0)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
app.session.userFlow.collect { (name, email) ->
header.findViewById<TextView>(R.id.header_name).text = name
header.findViewById<TextView>(R.id.header_email).text = email
header.findViewById<TextView>(R.id.header_avatar).text =
name.trim().take(1).uppercase().ifEmpty { "K" }
}
}
// Cart badge on the bottom navigation
launch {
app.repository.cartCount.collect { count ->
val badge = binding.bottomNav.getOrCreateBadge(R.id.cartFragment)
badge.isVisible = count > 0
badge.number = count
badge.backgroundColor = ContextCompat.getColor(this@MainActivity, R.color.sale)
}
}
// Offline banner
launch {
app.network.online.collect { online -> binding.offlineBanner.visibleIf(!online) }
}
}
}
// Load the cart count once (also checks that the token is still valid).
lifecycleScope.launch {
val r = app.repository.me()
if (r is com.codingeasily.karteasy.data.api.Result.Error && r.code == 401) {
app.session.clearLogin()
openAndFinish(AuthActivity::class.java)
}
}
}
private fun askNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
askNotifications.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
private fun shareApp() {
val send = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, getString(R.string.share_text))
}
startActivity(Intent.createChooser(send, getString(R.string.share_app)))
}
private fun showHelp() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.help_title)
.setMessage(R.string.help_message)
.setPositiveButton(R.string.ok, null)
.show()
}
fun confirmLogout() {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.logout)
.setMessage(R.string.logout_confirm)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.logout) { _, _ ->
lifecycleScope.launch {
kartApp.repository.logout()
binding.bottomNav.hide()
openAndFinish(AuthActivity::class.java)
}
}
.show()
}
override fun onSupportNavigateUp(): Boolean =
navController.navigateUp(appBarConfig) || super.onSupportNavigateUp()
companion object {
fun applyDarkMode(mode: String) {
AppCompatDelegate.setDefaultNightMode(
when (mode) {
"light" -> AppCompatDelegate.MODE_NIGHT_NO
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
)
}
}
}Step 5: Account screen
पहले letter वाले avatar के साथ gradient header, auth/me.php से तीन tap होने वाले counters, एक KartEasy.MenuRow style से बनी menu rows, theme switch और Logout।
<?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"
xmlns:tools="http://schemas.android.com/tools"
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:paddingBottom="24dp">
<!-- Profile header -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_header_gradient"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="20dp">
<TextView
android:id="@+id/avatar"
android:layout_width="64dp"
android:layout_height="64dp"
android:background="@drawable/bg_avatar"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="28sp"
android:textStyle="bold"
tools:text="R" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?attr/textAppearanceTitleLarge"
android:textColor="@android:color/white"
android:textStyle="bold"
tools:text="Riya Sharma" />
<TextView
android:id="@+id/email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#DDFFFFFF"
tools:text="test@karteasy.in" />
<TextView
android:id="@+id/phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#DDFFFFFF"
tools:text="9876543210" />
</LinearLayout>
</LinearLayout>
<!-- Counts -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="-16dp"
android:layout_marginEnd="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="12dp"
android:paddingBottom="12dp">
<LinearLayout
android:id="@+id/stat_orders"
style="@style/KartEasy.Stat">
<TextView
android:id="@+id/orders_count"
style="@style/KartEasy.StatNumber"
tools:text="4" />
<TextView
style="@style/KartEasy.StatLabel"
android:text="@string/orders" />
</LinearLayout>
<LinearLayout
android:id="@+id/stat_wishlist"
style="@style/KartEasy.Stat">
<TextView
android:id="@+id/wishlist_count"
style="@style/KartEasy.StatNumber"
tools:text="2" />
<TextView
style="@style/KartEasy.StatLabel"
android:text="@string/wishlist" />
</LinearLayout>
<LinearLayout
android:id="@+id/stat_cart"
style="@style/KartEasy.Stat">
<TextView
android:id="@+id/cart_count"
style="@style/KartEasy.StatNumber"
tools:text="3" />
<TextView
style="@style/KartEasy.StatLabel"
android:text="@string/cart" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Menu rows -->
<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">
<TextView
android:id="@+id/row_orders"
style="@style/KartEasy.MenuRow"
app:drawableStartCompat="@drawable/ic_orders"
android:text="@string/nav_orders" />
<TextView
android:id="@+id/row_addresses"
style="@style/KartEasy.MenuRow"
app:drawableStartCompat="@drawable/ic_location"
android:text="@string/nav_addresses" />
<TextView
android:id="@+id/row_wishlist"
style="@style/KartEasy.MenuRow"
app:drawableStartCompat="@drawable/ic_heart_outline"
android:text="@string/nav_wishlist" />
<TextView
android:id="@+id/row_profile"
style="@style/KartEasy.MenuRow"
app:drawableStartCompat="@drawable/ic_edit"
android:text="@string/title_edit_profile" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Theme -->
<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/theme" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/theme_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:selectionRequired="true"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
android:id="@+id/theme_system"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/theme_system" />
<com.google.android.material.button.MaterialButton
android:id="@+id/theme_light"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/theme_light" />
<com.google.android.material.button.MaterialButton
android:id="@+id/theme_dark"
style="@style/Widget.Material3.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/theme_dark" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.button.MaterialButton
android:id="@+id/logout_button"
style="@style/Widget.KartEasy.Button.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:text="@string/logout"
android:textColor="@color/sale" />
<TextView
android:id="@+id/version"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/text_secondary"
android:textSize="12sp"
tools:text="KartEasy v1.0 · Made with CodingEasily" />
</LinearLayout>
</ScrollView>package com.codingeasily.karteasy.ui.account
import android.app.Application
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import com.codingeasily.karteasy.BuildConfig
import com.codingeasily.karteasy.MainActivity
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.api.Result
import com.codingeasily.karteasy.data.model.User
import com.codingeasily.karteasy.databinding.FragmentAccountBinding
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.kartApp
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class AccountViewModel(app: Application) : AndroidViewModel(app) {
private val repo = app.kartApp.repository
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user
fun load() {
viewModelScope.launch {
val r = repo.me()
if (r is Result.Success) _user.value = r.data
}
}
}
class AccountFragment : Fragment() {
private var _binding: FragmentAccountBinding? = null
private val binding get() = _binding!!
private val vm: AccountViewModel by viewModels()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentAccountBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val nav = findNavController()
binding.rowOrders.setOnClickListener { nav.navigate(R.id.ordersFragment) }
binding.rowAddresses.setOnClickListener { nav.navigate(R.id.addressListFragment) }
binding.rowWishlist.setOnClickListener { nav.navigate(R.id.wishlistFragment) }
binding.rowProfile.setOnClickListener { nav.navigate(R.id.editProfileFragment) }
binding.statOrders.setOnClickListener { nav.navigate(R.id.ordersFragment) }
binding.statWishlist.setOnClickListener { nav.navigate(R.id.wishlistFragment) }
binding.statCart.setOnClickListener { nav.navigate(R.id.cartFragment) }
binding.logoutButton.setOnClickListener { (requireActivity() as MainActivity).confirmLogout() }
binding.version.text = getString(R.string.version_text, BuildConfig.VERSION_NAME)
// Theme: System / Light / Dark
viewLifecycleOwner.lifecycleScope.launch {
val mode = requireContext().kartApp.session.darkMode()
binding.themeGroup.check(
when (mode) {
"light" -> R.id.theme_light
"dark" -> R.id.theme_dark
else -> R.id.theme_system
}
)
binding.themeGroup.addOnButtonCheckedListener { _, id, checked ->
if (!checked) return@addOnButtonCheckedListener
val newMode = when (id) {
R.id.theme_light -> "light"
R.id.theme_dark -> "dark"
else -> "system"
}
viewLifecycleOwner.lifecycleScope.launch {
requireContext().kartApp.session.setDarkMode(newMode)
MainActivity.applyDarkMode(newMode)
}
}
}
collectWhenStarted(vm.user) { u ->
if (u == null) return@collectWhenStarted
binding.avatar.text = u.name.trim().take(1).uppercase()
binding.name.text = u.name
binding.email.text = u.email
binding.phone.text = u.phone ?: getString(R.string.add_phone)
binding.ordersCount.text = u.orderCount.toString()
binding.wishlistCount.text = u.wishlistCount.toString()
binding.cartCount.text = u.cartCount.toString()
}
}
override fun onResume() {
super.onResume()
vm.load()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}Dark mode switch
System / Light / Dark वाला MaterialButtonToggleGroup। Choice DataStore में save होती है और AppCompatDelegate.setDefaultNightMode() से लगती है — activity values-night colours के साथ फिर से बनती है। अगली बार शुरू होने पर MainActivity कुछ भी दिखाने से पहले saved mode लगा देती है।
System (default)
- phone की setting follow करे
- MODE_NIGHT_FOLLOW_SYSTEM
Light / Dark
- user सिर्फ इस app के लिए तय करे
- MODE_NIGHT_NO / MODE_NIGHT_YES
Step 6: Edit profile
Name और phone बदल सकते हैं; email field helper text के साथ disabled है। Password वाला हिस्सा optional है: "New password" भरने पर current password ज़रूरी है, बिल्कुल auth/update.php जैसा। Drawer header अपने आप update होता है क्योंकि वह session.userFlow observe करता है।
<?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"
app:startIconDrawable="@drawable/ic_person">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/name_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPersonName|textCapWords" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/email_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:enabled="false"
android:hint="@string/email"
app:helperText="@string/email_locked"
app:startIconDrawable="@drawable/ic_email">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/email_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textEmailAddress" />
</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 "
app:startIconDrawable="@drawable/ic_phone">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/phone_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone"
android:maxLength="10" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
style="@style/KartEasy.CardTitle"
android:layout_marginTop="24dp"
android:text="@string/change_password" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/current_password_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/current_password"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/current_password_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/new_password_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:hint="@string/new_password"
app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/new_password_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
<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" />
</LinearLayout>
</ScrollView>package com.codingeasily.karteasy.ui.account
import android.app.Application
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.fragment.findNavController
import com.codingeasily.karteasy.R
import com.codingeasily.karteasy.data.api.Result
import com.codingeasily.karteasy.data.model.User
import com.codingeasily.karteasy.databinding.FragmentEditProfileBinding
import com.codingeasily.karteasy.util.Validator
import com.codingeasily.karteasy.util.collectWhenStarted
import com.codingeasily.karteasy.util.kartApp
import com.codingeasily.karteasy.util.snack
import com.codingeasily.karteasy.util.toast
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 EditProfileViewModel(app: Application) : AndroidViewModel(app) {
private val repo = app.kartApp.repository
private val _user = MutableStateFlow<User?>(null)
val user: StateFlow<User?> = _user
private val _result = MutableSharedFlow<Result<User>>()
val result: SharedFlow<Result<User>> = _result
private val _saving = MutableStateFlow(false)
val saving: StateFlow<Boolean> = _saving
init {
viewModelScope.launch {
val r = repo.me()
if (r is Result.Success) _user.value = r.data
}
}
fun save(name: String, phone: String, current: String, new: String) {
if (_saving.value) return
_saving.value = true
viewModelScope.launch {
_result.emit(repo.updateProfile(name, phone, current, new))
_saving.value = false
}
}
}
class EditProfileFragment : Fragment() {
private var _binding: FragmentEditProfileBinding? = null
private val binding get() = _binding!!
private val vm: EditProfileViewModel by viewModels()
private var filled = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentEditProfileBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
filled = savedInstanceState != null
collectWhenStarted(vm.user) { u ->
if (u == null || filled) return@collectWhenStarted
filled = true
binding.nameInput.setText(u.name)
binding.emailInput.setText(u.email)
binding.phoneInput.setText(u.phone)
}
collectWhenStarted(vm.saving) { binding.saveButton.isEnabled = !it }
collectWhenStarted(vm.result) { r ->
when (r) {
is Result.Success -> { requireContext().toast(r.message); findNavController().navigateUp() }
is Result.Error -> snack(r.message)
}
}
binding.saveButton.setOnClickListener { save() }
}
private fun save() {
val name = binding.nameInput.text.toString()
val phone = binding.phoneInput.text.toString()
val current = binding.currentPasswordInput.text.toString()
val new = binding.newPasswordInput.text.toString()
binding.nameLayout.error = if (Validator.name(name)) null else getString(R.string.err_name)
binding.phoneLayout.error = if (phone.isEmpty() || Validator.phone(phone)) null else getString(R.string.err_phone)
binding.newPasswordLayout.error = if (new.isEmpty() || Validator.password(new)) null else getString(R.string.err_password)
binding.currentPasswordLayout.error = if (new.isNotEmpty() && current.isEmpty()) getString(R.string.err_current_password) else null
if (listOf(binding.nameLayout, binding.phoneLayout, binding.newPasswordLayout, binding.currentPasswordLayout).any { it.error != null }) return
vm.save(name, phone, current, new)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}Logout
Offline cache cart और orders जैसे API जवाब रखता है। उसी phone पर कोई और login करे तो उसे पिछले user का data कभी नहीं दिखना चाहिए, offline भी नहीं।
