Compose笔记(二十二)--NavController

news2025/5/13 3:41:13

        这一节主要了解一下Compose中的NavController,它是实现导航功能的核心组件,提供了强大而灵活的页面管理能力,用于管理导航图中的目的地和执行导航操作。

API
navigate(route: String)
含义:导航到指定路由的目的地。
作用:触发页面跳转,可以携带参数(如navigate("detail/{id}"))。
popBackStack()
含义:返回上一个目的地(从返回栈弹出当前页面)。
作用:模拟返回按钮行为,可指定是否弹出到特定路由。
popBackStack(route: String, inclusive: Boolean)
含义:弹出返回栈直到指定路由。
参数:
route:目标路由。
inclusive:是否包含目标路由(true则目标路由也会被弹出)。
currentBackStackEntry
含义:当前显示的目的地的NavBackStackEntry。
作用:获取当前页面的状态、参数或传递数据。
previousBackStackEntry
含义:返回栈中前一个目的地的NavBackStackEntry。
作用:与前一个页面通信(如返回数据)。

优点:
1. 声明式导航
与 Compose 风格一致:基于状态驱动,通过修改返回栈状态而非命令式调用实现导航,更符合Compose的设计哲学。
简化导航逻辑:无需手动管理 Activity/Fragment生命周期,导航状态自动保存与恢复。
2. 灵活的参数传递
支持多种参数类型:可传递基本类型、Parcelable对象,甚至通过savedStateHandle传递任意复杂数据。
深层链接友好:通过路由模板和NavType轻松实现带参数的外部链接导航。
缺点:
1复杂场景支持有限
动态路由困难:运行时动态添加或修改路由较复杂,需手动管理导航图。
跨模块导航繁琐:在模块化项目中,导航图的拆分和集成需要额外配置。

栗子:

import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.runtime.Composable

@Composable
fun NavigationExample() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(navController = navController)
        }
        composable("detail") {
            DetailScreen(navController = navController)
        }
    }
}

@Composable
fun HomeScreen(navController: NavHostController) {
    Button(onClick = { navController.navigate("detail") }) {
        Text("前往详情页")
    }
}

@Composable
fun DetailScreen(navController: NavHostController) {
    Button(onClick = { navController.popBackStack() }) {
        Text("返回首页")
    }
}
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.*
import androidx.navigation.compose.*
import androidx.compose.material.*
import androidx.compose.foundation.layout.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

@Composable
fun NavigationExampleTest() {
    val navController = rememberNavController()
    val authViewModel: AuthViewModel = viewModel()

    val isAuthenticated by authViewModel.isAuthenticated.collectAsState()

    NavHost(navController = navController, startDestination = "splash") {
        // 1. 闪屏页(检查登录状态)
        composable("splash") {
            SplashScreen(navController = navController)
        }

        // 2. 登录/注册流程
        composable("login") {
            LoginScreen(
                navController = navController,
                onLoginSuccess = { authViewModel.login() }
            )
        }

        navigation(route = "app", startDestination = "home") {
            composable("home") {
                HomeScreen2(navController = navController)
            }

            composable("cart") {
                if (isAuthenticated) {
                    CartScreen2(navController = navController)
                } else {
                    navController.currentBackStackEntry?.savedStateHandle?.set("targetRoute", "cart")
                    navController.navigate("login")
                }
            }

            composable("profile") {
                if (isAuthenticated) {
//                    ProfileScreen(navController = navController)
                } else {
                    navController.currentBackStackEntry?.savedStateHandle?.set("targetRoute", "profile")
                    navController.navigate("login")
                }
            }

            composable(
                route = "product/{productId}",
                arguments = listOf(navArgument("productId") { type = NavType.IntType }),
                deepLinks = listOf(
                    navDeepLink { uriPattern = "https://example.com/products/{productId}" },
                    navDeepLink { uriPattern = "myapp://products/{productId}" }
                )
            ) { backStackEntry ->
                val productId = backStackEntry.arguments?.getInt("productId") ?: 0
                ProductDetailScreen(
                    productId = productId,
                    navController = navController
                )
            }

        }
    }
}

@Composable
fun SplashScreen(navController: NavHostController) {
    val context = LocalContext.current
    LaunchedEffect(Unit) {
        delay(1500)
        val isLoggedIn = checkLoginStatus(context)

        if (isLoggedIn) {
            navController.navigate("app") {
                popUpTo("splash") { inclusive = true }
            }
        } else {
            navController.navigate("login") {
                popUpTo("splash") { inclusive = true }
            }
        }
    }

    Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
        CircularProgressIndicator()
    }
}

@Composable
fun LoginScreen(
    navController: NavHostController,
    onLoginSuccess: () -> Unit
) {
    var username by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        TextField(
            value = username,
            onValueChange = { username = it },
            label = { Text("用户名") }
        )
        TextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("密码") },
            visualTransformation = PasswordVisualTransformation()
        )
        Button(
            onClick = {
                onLoginSuccess()
                val targetRoute = navController.previousBackStackEntry?.savedStateHandle?.get<String>("targetRoute")
                if (targetRoute != null) {
                    navController.navigate("app/$targetRoute") {
                        popUpTo("login") { inclusive = true }
                    }
                } else {
                    navController.navigate("app") {
                        popUpTo("login") { inclusive = true }
                    }
                }
            }
        ) {
            Text("登录")
        }
    }
}

@Composable
fun HomeScreen2(navController: NavHostController) {
    Button(onClick = { navController.navigate("product/123") }) {
        Text("查看热门商品")
    }
}

@Composable
fun ProductDetailScreen(
    productId: Int,
    navController: NavHostController
) {
    val product = remember { Product2(productId, "商品$productId", 99.99) }
    Column {
        Text(product.name, style = MaterialTheme.typography.h4)
        Text("¥${product.price}", style = MaterialTheme.typography.h5)

    }
}

@Composable
fun CartScreen2(navController: NavHostController) {
    val cartViewModel: CartViewModel = viewModel()
    val cartItems by cartViewModel.cartItems.collectAsState()
    Column {
        Text("购物车 (${cartItems.size})")
        Button(onClick = {
            // 创建订单并导航到结算页
            val orderId = createOrder(cartItems)
            navController.navigate("app/checkout/$orderId")
        }) {
            Text("结算")
        }
    }
}

data class Product2(val id: Int, val name: String, val price: Double)
data class CartItem(val product: Product, val quantity: Int)

private fun checkLoginStatus(context: android.content.Context): Boolean {
    return false
}

private fun createOrder(cartItems: List<CartItem>): String {
    return System.currentTimeMillis().toString()
}

class AuthViewModel : ViewModel() {
    private val _isAuthenticated = MutableStateFlow(false)
    val isAuthenticated: StateFlow<Boolean> = _isAuthenticated

    fun login() {
        _isAuthenticated.value = true
    }

    fun logout() {
        _isAuthenticated.value = false
    }
}

class CartViewModel : ViewModel() {
    private val _cartItems = MutableStateFlow(emptyList<CartItem>())
    val cartItems: StateFlow<List<CartItem>> = _cartItems

    fun addToCart(product: Product, quantity: Int = 1) {
        // 更新购物车逻辑...
    }
}

注意:
1 避免深层嵌套
过度嵌套(如三层以上导航)会导致返回栈管理复杂,建议通过参数传递替代深层导航。
示例:用单个详情页接收不同参数,替代多个层级的详情子页。
2 合理拆分导航图
在模块化项目中,按功能拆分导航图(如用户模块、商品模块),避免单个导航图过大。
使用androidx.navigation:navigation-dynamic-features支持动态加载导航图。
3 明确路由命名
使用语义化路由(如product/{id}而非screen3),提高代码可读性。
避免路由参数与路径混淆(如product?id=123 vs product/123)。
4 避免大对象传递
路由参数(如arguments)应尽量传递 ID 而非完整对象,防止内存占用过高。
示例:传递商品 ID 而非整个Product对象,在目标页面通过 ID 加载数据。
5 合理使用savedStateHandle
仅用于短期数据传递(如页面返回结果),避免存储大量或长期数据。
及时清理不再需要的数据,防止内存泄漏。

源码:

1. 导航操作入口
navigate函数是触发导航的主要入口,它接收一个路由字符串作为参数,用于指定要导航到的目标屏幕。

public fun navigate(route: String, builder: NavOptionsBuilder.() -> Unit = {}) {
    val navOptions = NavOptionsBuilder().apply(builder).build()
    navigate(route, navOptions)
}

这里首先通过NavOptionsBuilder构建导航选项,然后调用另一个navigate重载方法。
2. 路由解析与匹配
在navigate方法内部,会通过Navigator来解析路由并找到对应的目的地。

val navigator = navigatorProvider.getNavigator(route)
val destination = navigator.findDestination(route)

navigatorProvider是一个用于获取不同类型Navigator的提供者,它根据路由的类型(例如,是否是深层链接等)来获取相应的Navigator。然后,通过Navigator的findDestination方法在导航图中查找与路由匹配的Destination。
3. 处理导航逻辑
找到目标Destination后,会根据导航选项(如是否添加到返回栈、是否启动新任务等)来执行具体的导航逻辑。

if (navOptions.shouldLaunchSingleTop && currentBackStackEntry?.destination == destination) {
    // 处理单顶模式,即如果目标屏幕已经在栈顶,执行相应的回调
    onDestinationReached(destination)
    return
}

val newBackStackEntry = createBackStackEntry(destination, navOptions)
if (navOptions.addToBackStack) {
    // 如果需要添加到返回栈,将新的BackStackEntry添加到返回栈中
    backStack.add(newBackStackEntry)
}
// 执行导航,更新当前显示的屏幕
setCurrentBackStackEntry(newBackStackEntry)

这里首先处理了单顶模式的情况,如果目标屏幕已经在栈顶,会执行相应的回调。然后创建一个新的BackStackEntry,它包含了目标屏幕的相关信息。如果导航选项要求添加到返回栈,就将新的BackStackEntry添加到返回栈中。最后,通过setCurrentBackStackEntry方法更新当前显示的屏幕,这会触发Composable的重新组合,以显示新的屏幕。

4. 返回操作逻辑
当执行返回操作时,例如通过系统返回键或者调用popBackStack方法,会从返回栈中弹出当前的BackStackEntry。

public fun popBackStack(): Boolean {
    val currentEntry = currentBackStackEntry ?: return false
    val newIndex = backStack.indexOf(currentEntry) - 1
    if (newIndex < 0) {
        // 已经在栈底,无法返回
        return false
    }
    // 弹出当前BackStackEntry
    backStack.removeAt(newIndex + 1)
    // 设置新的当前BackStackEntry
    setCurrentBackStackEntry(backStack[newIndex])
    return true
}

首先获取当前的BackStackEntry,然后计算返回后的索引位置。如果索引小于 0,表示已经在栈底,无法返回。否则,从返回栈中移除当前的BackStackEntry,并设置新的当前BackStackEntry,从而实现返回上一个屏幕的功能。

        简单总结,NavController通过这种基于返回栈的状态管理和路由匹配机制,实现了Compose中灵活且高效的导航功能,使得开发者能够方便地管理应用中的屏幕导航和状态切换。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2374368.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Ubuntu 22.04.5 LTS 基于 kubesphere 安装 cube studio

Ubuntu 22.04.5 LTS 基于 kubesphere 安装 cube studio 前置条件 已经成功安装 kubesphere v4.3.1 参考教程: https://github.com/data-infra/cube-studio/wiki/%E5%9C%A8-kubesphere-%E4%B8%8A%E6%90%AD%E5%BB%BA-cube-studio 1. 安装基础依赖 # ubuntu安装基础依赖 apt insta…

1.短信登录

1.0 问题记录 1.0.1 redis 重复 token 问题 每次用户登录时&#xff0c;后端会创建一个新的 token 并存入 Redis&#xff0c;但之前登录的 token 还没有过期。这可能会导致以下问题&#xff1a; 1. Redis 中存在大量未过期但实际已不使用的 token2. 同一用户可能有多个有效 …

Linux-Ubuntu安装Stable Diffusion Forge

SD Forge在Win上配置起来相对简单且教程丰富&#xff0c;而在Linux平台的配置则稍有门槛且教程较少。本文提供一个基于Ubuntu24.04发行版&#xff08;对其他Linux以及SD分支亦有参考价值&#xff09;的Stable Diffusion ForgeUI安装配置教程&#xff0c;希望有所帮助 本教程以N…

MixTeX - 支持CPU推理的多模态LaTeX OCR

文章目录 一、项目概览相关资源核心特性技术特点 二、安装三、使用说明环境要求 四、版本更新五、当前限制 一、项目概览 MixTeX是一款创新的多模态LaTeX识别小程序&#xff0c;支持本地离线环境下的高效CPU推理。 无论是LaTeX公式、表格还是混合文本&#xff0c;MixTeX都能轻…

23、DeepSeek-V2论文笔记

DeepSeek-V2 1、背景2、KV缓存优化2.0 KV缓存&#xff08;Cache&#xff09;的核心原理2.1 KV缓存优化2.2 性能对比2.3 架构2.4多头注意力 &#xff08;MHA&#xff09;2.5 多头潜在注意力 &#xff08;MLA&#xff09;2.5.1 低秩键值联合压缩 &#xff08;Low-Rank Key-Value …

【算法专题十一】字符串

文章目录 1. leetcode.14.最长公共前缀1.1 题目1.2 思路1.3 代码 2. leetcode.5.最长回文字串2.1 题目2.2 思路2.3 代码 3. leetcode.67.二进制求和3.1 题目3.2 思路3.3 代码 4. leetcode.43.字符串相乘4.1 题目4.2 思路4.3 代码 1. leetcode.14.最长公共前缀 1.1 题目 题目链…

美化IDEA注释:Idea 中快捷键 Ctrl + / 自动注释的缩进(避免添加注释自动到行首)以及 Ctrl + Alt + l 全局格式化代码的注释缩进

打开 Settings 界面&#xff0c;依次选择 Editor -> Code Style -> Java&#xff0c;选择 Code Generation&#xff0c; 取消 Line comment at first column 和 Block comment at first column 的勾选即可&#xff0c; 1、Line comment at first column (行注释在第一列…

赛灵思 XCZU11EG-2FFVC1760I XilinxFPGAZynq UltraScale+ MPSoC EG

XCZU11EG-2FFVC1760I 是 Zynq UltraScale MPSoC EG 系列中性能最强的器件之一&#xff0c;集成了四核 ARM Cortex-A53 应用处理器、双核 Cortex-R5 实时处理器与 Mali-400 MP2 GPU&#xff0c;并结合了 653,100 个逻辑单元与丰富的片上存储资源&#xff0c;可满足高性能计算、A…

Android Camera HAL v3 and Video4Linux 2

《小驰行动派的知识星球》 ———————————————— 推荐阅读&#xff1a; 关于博主 《小驰Camera私房菜》小册目录 采用v4l2loopback来实现 虚拟Camera Camera基础及一些基本概念 Android Camera 学习路线 | 个人推荐 Android Camera开发系列&#xff08;干货满满&a…

基于pyqt的上位机开发

目录 安装依赖 功能包含 运行结果 安装依赖 pip install pyqt5 pyqtgraph pyserial 功能包含 自动检测串口设备&#xff0c;波特率选择/连接断开控制&#xff0c;数据发送/接收基础框架&#xff0c;实时绘图区域&#xff08;需配合数据解析&#xff09; ""&q…

CentOS 7 系统下安装 OpenSSL 1.0.2k 依赖问题的处理

前面有提到过这个openssl的版本冲突问题&#xff0c;也是在这次恢复服务器时遇到的问题&#xff0c;我整理如下&#xff0c;供大家参考。小小一个软件的安装&#xff0c;挺坑的。 一、问题 项目运行环境需要&#xff0c;指定PHP7.0.9这个版本&#xff0c;但是‌系统版本与软件…

vue修改了node_modules中的包,打补丁

1、安装patch npm i patch-package 安装完成后&#xff0c;会在package.json中显示版本号 2、在package.json的scripts中增加配置 "postinstall": "patch-package" 3、执行命令 npx patch-package 修改的node_modules中的包的名称 像这样 npx patch-packag…

[matlab]private和+等特殊目录在新版本matlab中不允许添加搜索路径解决方法

当我们目录包含有private,或者时候matlab搜索目录不让添加&#xff0c;比如截图&#xff1a; 在matlab2018以前这些都可以加进去后面版本都不行了。但是有时候我们必须要加进去才能兼容旧版本matlab库&#xff0c;比如mexopencv库就是这种情况。因此我们必须找到一个办法加进去…

OpenTelemetry 介绍

文章目录 1. 概述什么是OpenTelemetry发展历史与背景主要特点与优势2. 核心概念追踪(Tracing)指标(Metrics)日志(Logs)行李(Baggage)3. 主要组件API层SDK层数据收集器(Collector)导出器(Exporters)OTLP(OpenTelemetry Protocol)4. 集成方式语言支持(SDK)自动与手动插桩常见框…

【连载14】基础智能体的进展与挑战综述-多智能体系统设计

基础智能体的进展与挑战综述 从类脑智能到具备可进化性、协作性和安全性的系统 【翻译团队】刘军(liujunbupt.edu.cn) 钱雨欣玥 冯梓哲 李正博 李冠谕 朱宇晗 张霄天 孙大壮 黄若溪 在基于大语言模型的多智能体系统&#xff08;LLM-MAS&#xff09;中&#xff0c;合作目标和合…

blender云渲染指南2025版

一、云渲染核心概念 Blender云渲染是将本地渲染任务迁移到云端服务器集群的技术&#xff0c;通过分布式计算实现效率提升100倍以上的解决方案&#xff0c;其核心逻辑是&#xff1a;用户上传Blender项目文件至【渲染101】等云平台&#xff0c;云端调用高性能服务器&#xff08;…

Mysql-OCP PPT课程讲解并翻译

#跳过介绍&#xff0c;直接从干货开始记录 第一章 安装mysql windows安装

加速项目落地(Trae编辑器)

目录 vscode安装python支持 vscode常用插件 Trae编辑器 两个界面合成 补充&#xff08;QT开发的繁琐&#xff09; AI编程哪家强&#xff1f;Cursor、Trae深度对比&#xff0c;超详细&#xff01; - 知乎 Trae兼容vscode的插件&#xff0c;我们可以先在vscode里面装好再一…

配置 Web 服务器练习

一、要求 1.通过https://ip 可以访问到网站首页 2.通过 https://ip/private/ 实现用户访问控制&#xff0c;仅允许已经添加的 tom&#xff0c;jerry 能够访问到 private 子路径的界面 3.通过 https://ip/vrit/ 实现能够访问到将系统 /nginx/virt 目录下的网页文件&#xff0…

Python程序,输入IP,扫描该IP哪些端口对外是开放的,输出端口列表

#!/usr/bin/env python # -*- coding: utf-8 -*-""" IP端口扫描程序 输入IP地址&#xff0c;扫描该IP哪些端口对外是开放的&#xff0c;输出端口列表 """import socket import sys import concurrent.futures import ipaddress from tabulate im…