【Compose multiplatform教程24】导航与路由
导航库目前处于实验阶段。
导航是用户界面(UI)应用程序的关键部分,它能让用户在应用程序的不同屏幕之间进行切换。Compose 多平台采用了 Jetpack Compose 的导航方法。
设置
要使用导航库,需将以下依赖项添加到你的 commonMain
源集中:
kotlin {// ...sourceSets {// ...commonMain.dependencies {// ...implementation("org.jetbrains.androidx.navigation:navigation-compose:2.8.0-alpha10")}// ...}
}
类型安全的导航。
从 1.7.0 版本开始,Compose 多平台在通用代码中支持如 Jetpack 文档所述的类型安全的导航。
示例项目
要查看 Compose 多平台导航库的实际使用情况,请查看 “ nav_cupcake projecthttps://github.com/JetBrains/compose-multiplatform/tree/master/examples/nav_cupcake” 项目,该项目是由 Navigate between screens with Compose Compose 在安卓(Android)中实现屏幕间导航https://developer.android.com/codelabs/basic-android-kotlin-compose-navigation#0的代码实验室(codelab)转换而来的。
与 Jetpack Compose 一样,要实现导航,你应当:
1:列出应包含在导航图中的路由List routes 。每条路由都必须是定义了路径的唯一字符串。
2:创建一个 NavHostController
实例NavHostController,将其作为主要的可组合属性来管理导航。
3:向你的应用添加一个 NavHost
可组合项:
从你之前定义的路由列表中选择起始目的地。
创建一个导航图,可以直接创建(作为创建
NavHost
的一部分),也可以通过编程方式使用NavController.createGraph()
函数来创建。
每个后台栈条目(导航图中包含的每条导航路由)都实现了 LifecycleOwner
接口。在应用的不同屏幕之间进行切换时,其状态会从 “已恢复(RESUMED)” 变为 “已启动(STARTED)”,然后再变回去。“已恢复(RESUMED)” 状态也被描述为 “已稳定”:当新屏幕准备就绪且处于活动状态时,就认为导航已完成。有关 Compose 多平台中当前实现的详细信息,请参阅 “生命周期(Lifecycle)” 页面。
局限性;限制条件
与 Jetpack Compose 相比,Compose 多平台中导航功能的当前限制如下:
1:不支持深度链接Deep links(处理或跟踪深度链接)。
2:除安卓(Android)平台外,在任何其他平台上均不支持 BackHandlerhttps://developer.android.com/develop/ui/compose/libraries#handling_the_system_back_button 函数以及预测性返回手势。predictive back gestureshttps://developer.android.com/guide/navigation/custom-back/predictive-back-gesture
第三方替代方案
例如在某个软件功能领域,如果官方提供的方案存在一些局限或者不能满足特定需求时,就可以去寻找由第三方开发者或团队所提供的其他可替代的解决方案来满足相应需求.
如果 Compose 多平台导航组件无法解决你的问题,还有一些第三方替代方案可供你选择:
Name | Description |
---|---|
Voyager | 一种务实的导航方法。 |
Decompose | 一种涵盖整个生命周期以及任何潜在依赖注入的高级导航方法。 |
Appyx | 带有手势控制的模型驱动导航。 |
PreCompose | 一种受 Jetpack 生命周期、视图模型、实时数据以及导航组件启发的导航和视图模型 |
Circuit | 一种适用于具有导航和高级状态管理功能的 Kotlin 应用程序的、由 Compose 驱动的架构。 |
完整示例
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement.Absolute.Center
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifierimport androidx.navigation.NavHostControllerimport androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AccountBox
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.HorizontalDivider
import androidx.compose.ui.Alignment
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.resources.StringResource
import androidx.navigation.compose.rememberNavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import kotlinmultiplatform.composeapp.generated.resources.*
import kotlinmultiplatform.composeapp.generated.resources.Res
import kotlinmultiplatform.composeapp.generated.resources.home
import kotlinmultiplatform.composeapp.generated.resources.mine
import kotlinmultiplatform.composeapp.generated.resources.order
import org.jetbrains.compose.resources.stringResource
import org.lxz.project.compose.demo.ScaffoldDemo.imageVectorobject NavigationApp {enum class MainTabsMenu(val title: StringResource) {HOME(title = Res.string.home),ORDER(title = Res.string.order),MINE(title = Res.string.mine),SEARCH(title = Res.string.search);}val homeIcon = Icons.Filled.Homeval orderIcon = Icons.Filled.Searchval mineIcon = Icons.Filled.AccountBox@Composable
fun MainNavigationApp(navController: NavHostController = rememberNavController()) {val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route// Get current back stack entryval backStackEntry by navController.currentBackStackEntryAsState()// Get the name of the current screenScaffold(topBar = {TopAppBar(modifier = Modifier.height(56.dp), // 设置顶部栏高度为56dp示例title = {Text(text = "My Awesome App")},// 右侧操作按钮(示例添加一个搜索按钮)actions = {IconButton(onClick = {navController.navigate(MainTabsMenu.SEARCH.name)}) {Icon(imageVector = imageVector, contentDescription = "Search")}})},bottomBar = {BottomNavigation(modifier = Modifier.fillMaxWidth()) {BottomNavigationItem(icon = { homeIcon },label = { Text(text = stringResource(MainTabsMenu.HOME.title)) },selected = currentRoute == MainTabsMenu.HOME.name,onClick = {navController.navigate(MainTabsMenu.HOME.name) {}},alwaysShowLabel = true)BottomNavigationItem(icon = { orderIcon },label = { Text(text = stringResource(MainTabsMenu.ORDER.title)) },selected = currentRoute == MainTabsMenu.ORDER.name,onClick = {navController.navigate(MainTabsMenu.ORDER.name) {}},alwaysShowLabel = true)BottomNavigationItem(icon = { mineIcon },label = { Text(text = stringResource(MainTabsMenu.MINE.title)) },selected = currentRoute == MainTabsMenu.ORDER.name,onClick = {navController.navigate(MainTabsMenu.MINE.name) {}},alwaysShowLabel = true)}}) { innerPadding ->NavHost(navController = navController,startDestination = MainTabsMenu.HOME.name,modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(innerPadding)) {composable(route = MainTabsMenu.HOME.name) {bottomMenuItem(MainTabsMenu.HOME)}composable(route = MainTabsMenu.ORDER.name) {bottomMenuItem(MainTabsMenu.ORDER)}composable(route = MainTabsMenu.MINE.name) {bottomMenuItem(MainTabsMenu.MINE)}composable(route = MainTabsMenu.SEARCH.name) {bottomMenuItem(MainTabsMenu.SEARCH)}}}}
}@Composable
fun bottomMenuItem(item: NavigationApp.MainTabsMenu) {Column(verticalArrangement = Arrangement.Center,horizontalAlignment = Alignment.CenterHorizontally,modifier = Modifier.fillMaxWidth().fillMaxHeight().background(Color.White),) {Text(textAlign = TextAlign.Center,text = stringResource(item.title),)}}
运行效果: