
作为一名前端开发工程师,在掌握了 Compose 的基础组件、布局系统、状态管理、Material 3 组件库和自定义组件开发后,我开始探索如何构建完整的多页面应用。本文将从前端开发者的视角,深入解析 Compose 应用架构设计和导航系统的实现。
在前端开发中,我们习惯使用 React Router、Vue Router 等路由库来管理多页面应用。这些路由系统提供了声明式的路由配置、参数传递、嵌套路由等功能。Jetpack Compose 虽然也提供了官方的 Navigation Compose 库,但在本模块中,我将从零开始实现一个简单的导航系统,帮助理解导航的核心概念。
通过本模块的学习,我将探索:
前端框架 | Compose | 说明 |
|---|---|---|
React Router |
| 官方导航库 |
|
| 路由定义 |
|
| 导航控制 |
| 路由参数 | 参数获取 |
|
| 内容插槽 |
|
| 页面跳转 |
|
| 返回上一页 |
React Router 示例:
// React Router 路由定义
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<TodoList />} />
<Route path="/todos/:id" element={<TodoDetail />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</BrowserRouter>
);
}Vue Router 示例:
// Vue Router 路由定义
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{ path: '/', component: Home },
{ path: '/todos', component: TodoList },
{ path: '/todos/:id', component: TodoDetail },
{ path: '/settings', component: Settings }
];
const router = createRouter({
history: createWebHistory(),
routes
});Compose 自定义路由:
// Compose 类型安全的路由定义
sealed class AppRoute {
data object Home : AppRoute()
data object TodoList : AppRoute()
data class TodoDetail(val todoId: String) : AppRoute()
data object Settings : AppRoute()
}核心差异:
在深入学习之前,让我们先运行示例项目,直观感受导航系统的效果。
git clone https://github.com/easonxie/learn-jetpack-compose.git
cd learn-jetpack-composeWeb 版本:
# 启动 Web 开发服务器
./gradlew :lesson-06-app-architecture-navigation:wasmJsBrowserDevelopmentRun
# 访问地址
http://localhost:8080
开发模式(自动重新加载):
./gradlew :lesson-06-app-architecture-navigation:wasmJsBrowserDevelopmentRun --continuous使用 sealed class 定义应用的所有路由:
/**
* 应用路由定义
* 定义应用中所有可导航的页面
*/
sealed class AppRoute {
data object Home : AppRoute()
data object TodoList : AppRoute()
data class TodoDetail(val todoId: String) : AppRoute()
data object Settings : AppRoute()
data object Profile : AppRoute()
data object NavigationExample : AppRoute()
data object StateExample : AppRoute()
}为什么使用 sealed class?
前端对比:
// TypeScript 类型定义(运行时仍是字符串)
type AppRoute =
| { type: 'home' }
| { type: 'todoList' }
| { type: 'todoDetail', todoId: string }
| { type: 'settings' };
// 使用时仍需要字符串路径
<Route path="/todos/:id" element={<TodoDetail />} />为每个路由定义元数据:
/**
* 路由信息
*/
data class RouteInfo(
val route: AppRoute,
val title: String,
val icon: String
)
/**
* 获取路由信息
*/
fun getRouteInfo(route: AppRoute): RouteInfo {
return when (route) {
AppRoute.Home -> RouteInfo(route, "首页", "Home")
AppRoute.TodoList -> RouteInfo(route, "任务列表", "List")
is AppRoute.TodoDetail -> RouteInfo(route, "任务详情", "View")
AppRoute.Settings -> RouteInfo(route, "设置", "Settings")
AppRoute.Profile -> RouteInfo(route, "个人资料", "Person")
AppRoute.NavigationExample -> RouteInfo(route, "导航示例", "Navigation")
AppRoute.StateExample -> RouteInfo(route, "状态管理", "Refresh")
}
}定义在底部导航栏显示的路由:
/**
* 底部导航栏显示的路由
*/
val bottomNavRoutes = listOf(
AppRoute.Home,
AppRoute.TodoList,
AppRoute.Settings
)前端对比:
前端框架 | 导航管理 | Compose 对应 |
|---|---|---|
React |
|
|
Vue |
|
|
|
| 页面跳转 |
|
| 返回上一页 |
|
| 当前路由 |
NavigationManager 实现:
/**
* 简单的导航管理器
* 管理当前页面状态和导航历史
*/
class NavigationManager {
// 当前路由状态
private var _currentRoute = mutableStateOf<AppRoute>(AppRoute.Home)
val currentRoute: State<AppRoute> = _currentRoute
// 导航回退栈
private val _backStack = mutableStateListOf<AppRoute>()
val backStack: List<AppRoute> = _backStack
init {
_backStack.add(AppRoute.Home)
}
/**
* 导航到指定路由
*/
fun navigateTo(route: AppRoute) {
_currentRoute.value = route
// 如果不是返回操作,添加到回退栈
if (_backStack.isEmpty() || _backStack.last() != route) {
_backStack.add(route)
}
}
/**
* 导航到指定路由并清空回退栈
*/
fun navigateToAndClearStack(route: AppRoute) {
_currentRoute.value = route
_backStack.clear()
_backStack.add(route)
}
/**
* 返回上一页
*/
fun navigateBack(): Boolean {
if (_backStack.size > 1) {
_backStack.removeLastOrNull()
_currentRoute.value = _backStack.lastOrNull() ?: AppRoute.Home
return true
}
return false
}
/**
* 是否可以返回
*/
fun canNavigateBack(): Boolean = _backStack.size > 1
/**
* 获取当前路由信息
*/
fun getCurrentRouteInfo(): RouteInfo = getRouteInfo(_currentRoute.value)
}/**
* 记住导航管理器实例
*/
@Composable
fun rememberNavigationManager(): NavigationManager {
return remember { NavigationManager() }
}前端对比:
// React - useNavigate Hook
import { useNavigate } from 'react-router-dom';
function MyComponent() {
const navigate = useNavigate();
const handleClick = () => {
navigate('/todos');
};
return <button onClick={handleClick}>Go to Todos</button>;
}// Vue - useRouter Composable
import { useRouter } from 'vue-router';
export default {
setup() {
const router = useRouter();
const handleClick = () => {
router.push('/todos');
};
return { handleClick };
}
}前端对比:
// React 应用布局
function AppLayout() {
return (
<div className="app">
<Header />
<main>
<Outlet /> {/* 路由内容插槽 */}
</main>
<BottomNav />
</div>
);
}Compose Scaffold 实现:
/**
* 主应用脚手架
*/
@Composable
fun AppScaffold(
navigationManager: NavigationManager,
modifier: Modifier = Modifier,
content: @Composable (
route: AppRoute,
onNavigate: (AppRoute) -> Unit,
onBack: () -> Unit
) -> Unit
) {
val currentRoute by navigationManager.currentRoute
val canNavigateBack = navigationManager.canNavigateBack()
val routeInfo = navigationManager.getCurrentRouteInfo()
Scaffold(
modifier = modifier,
topBar = {
AppTopBar(
title = routeInfo.title,
canNavigateBack = canNavigateBack && currentRoute !in bottomNavRoutes,
onNavigateBack = { navigationManager.navigateBack() }
)
},
bottomBar = {
AppBottomNavigation(
currentRoute = currentRoute,
onNavigate = { route ->
navigationManager.navigateToAndClearStack(route)
}
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
content(
currentRoute,
{ route -> navigationManager.navigateTo(route) },
{ navigationManager.navigateBack() }
)
}
}
}/**
* 应用顶部栏
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppTopBar(
title: String,
canNavigateBack: Boolean = false,
onNavigateBack: () -> Unit = {},
modifier: Modifier = Modifier
) {
TopAppBar(
title = {
ChineseText(
text = title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold
)
},
navigationIcon = {
if (canNavigateBack) {
IconButton(onClick = onNavigateBack) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "返回"
)
}
}
},
modifier = modifier
)
}/**
* 底部导航栏组件
*/
@Composable
fun AppBottomNavigation(
currentRoute: AppRoute,
onNavigate: (AppRoute) -> Unit,
modifier: Modifier = Modifier
) {
NavigationBar(modifier = modifier) {
bottomNavRoutes.forEach { route ->
val routeInfo = getRouteInfo(route)
val selected = currentRoute == route
NavigationBarItem(
icon = {
Icon(
imageVector = getRouteIcon(route),
contentDescription = routeInfo.title
)
},
label = {
ChineseText(
text = routeInfo.title,
style = MaterialTheme.typography.labelMedium
)
},
selected = selected,
onClick = { onNavigate(route) }
)
}
}
}/**
* 主应用入口
*/
@Composable
fun NavigationApp() {
val navigationManager = rememberNavigationManager()
AppTheme {
AppScaffold(
navigationManager = navigationManager
) { currentRoute, onNavigate, onBack ->
// 根据当前路由显示对应页面
when (currentRoute) {
AppRoute.Home -> {
HomeScreen(onNavigate = onNavigate)
}
AppRoute.TodoList -> {
TodoListScreen(onNavigate = onNavigate)
}
is AppRoute.TodoDetail -> {
TodoDetailScreen(
todoId = currentRoute.todoId,
onBack = onBack
)
}
AppRoute.Settings -> {
SettingsScreen()
}
AppRoute.Profile -> {
ProfileScreen(onNavigate = onNavigate)
}
AppRoute.NavigationExample -> {
NavigationExampleScreen(
onNavigate = onNavigate,
onBack = onBack
)
}
AppRoute.StateExample -> {
StateExampleScreen(onBack = onBack)
}
}
}
}
}前端对比:
// React Router 路由映射
function App() {
return (
<BrowserRouter>
<AppLayout>
<Routes>
<Route path="/" element={<HomeScreen />} />
<Route path="/todos" element={<TodoListScreen />} />
<Route path="/todos/:id" element={<TodoDetailScreen />} />
<Route path="/settings" element={<SettingsScreen />} />
</Routes>
</AppLayout>
</BrowserRouter>
);
}Compose 类型安全的参数传递:
// 定义带参数的路由
sealed class AppRoute {
data class TodoDetail(val todoId: String) : AppRoute()
}
// 导航时传递参数
onNavigate(AppRoute.TodoDetail(todoId = "123"))
// 页面接收参数
@Composable
fun TodoDetailScreen(
todoId: String, // 类型安全的参数
onBack: () -> Unit
) {
ChineseText("任务 ID: $todoId")
}前端对比:
// React Router - 参数传递
// 1. 导航时传递
navigate('/todos/123');
// 2. 页面接收
import { useParams } from 'react-router-dom';
function TodoDetailScreen() {
const { id } = useParams(); // 类型是 string | undefined
return <div>任务 ID: {id}</div>;
}// Vue Router - 参数传递
// 1. 导航时传递
router.push({ name: 'todoDetail', params: { id: '123' } });
// 2. 页面接收
import { useRoute } from 'vue-router';
export default {
setup() {
const route = useRoute();
const id = route.params.id; // 类型是 string | string[]
return { id };
}
}优势:
/**
* 首页组件
*/
@Composable
fun HomeScreen(
onNavigate: (AppRoute) -> Unit,
modifier: Modifier = Modifier
) {
val scrollState = rememberScrollState()
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// 欢迎区域
WelcomeSection()
// 学习模块导航
LearningModulesSection(onNavigate = onNavigate)
// 功能特性介绍
FeaturesSection()
}
}/**
* 功能卡片组件
*/
@Composable
fun FeatureCard(
title: String,
description: String,
icon: ImageVector,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
onClick = onClick,
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Icon(
imageVector = icon,
contentDescription = title,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary
)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
ChineseText(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
ChineseText(
text = description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}// 导航到其他页面
FeatureCard(
title = "导航系统",
description = "学习页面导航、路由管理和参数传递",
icon = Icons.Default.ArrowForward,
onClick = { onNavigate(AppRoute.NavigationExample) }
)
// 导航到带参数的页面
FeatureCard(
title = "任务详情",
description = "查看任务的详细信息",
icon = Icons.Default.Info,
onClick = { onNavigate(AppRoute.TodoDetail(todoId = "123")) }
)/**
* 设置页面组件
*/
@Composable
fun SettingsScreen(
modifier: Modifier = Modifier
) {
var isDarkTheme by remember { mutableStateOf(false) }
val scrollState = rememberScrollState()
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
// 页面标题
PageTitle(
title = "设置",
subtitle = "个性化您的应用体验"
)
// 外观设置
AppearanceSection(
isDarkTheme = isDarkTheme,
onThemeChange = { isDarkTheme = it }
)
// 应用信息
AppInfoSection()
}
}/**
* 外观设置区域
*/
@Composable
private fun AppearanceSection(
isDarkTheme: Boolean,
onThemeChange: (Boolean) -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Icon(
imageVector = if (isDarkTheme) Icons.Default.DarkMode
else Icons.Default.LightMode,
contentDescription = "主题图标",
tint = MaterialTheme.colorScheme.primary
)
ChineseText(
text = "外观",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold
)
}
// 主题切换
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
ChineseText(
text = "深色主题",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
ChineseText(
text = if (isDarkTheme) "当前使用深色主题" else "当前使用浅色主题",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
checked = isDarkTheme,
onCheckedChange = onThemeChange
)
}
}
}
}前端对比:
React/Vue 单向数据流:
State → View → Event → State
Compose 单向数据流:
State → UI → Event → State实现示例:
// 状态向下传递
@Composable
fun ParentComponent() {
var count by remember { mutableIntStateOf(0) }
ChildComponent(
count = count, // 状态向下传递
onIncrement = { count++ } // 事件向上传递
)
}
@Composable
fun ChildComponent(
count: Int,
onIncrement: () -> Unit
) {
Button(onClick = onIncrement) {
ChineseText("计数: $count")
}
}前端对比:
// React - 状态提升
function ParentComponent() {
const [value, setValue] = useState('');
return (
<div>
<ChildInput value={value} onChange={setValue} />
<ChildDisplay value={value} />
</div>
);
}Compose 实现:
// Compose - 状态提升
@Composable
fun ParentComponent() {
var value by remember { mutableStateOf("") }
Column {
ChildInput(
value = value,
onValueChange = { value = it }
)
ChildDisplay(value = value)
}
}
@Composable
fun ChildInput(
value: String,
onValueChange: (String) -> Unit
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange
)
}
@Composable
fun ChildDisplay(value: String) {
ChineseText("当前值: $value")
}架构层次:
UI 层(Composable 函数)
↓
导航层(NavigationManager)
↓
业务逻辑层(ViewModel - 待实现)
↓
数据层(Repository - 待实现)当前实现:
// UI 层 - 只负责显示
@Composable
fun HomeScreen(onNavigate: (AppRoute) -> Unit) {
// UI 组件
}
// 导航层 - 只负责导航
class NavigationManager {
fun navigateTo(route: AppRoute) { /* ... */ }
}
// 业务逻辑层 - 待实现
// class TodoViewModel { /* ... */ }
// 数据层 - 待实现
// class TodoRepository { /* ... */ }// 首页导航到列表页
Button(onClick = { onNavigate(AppRoute.TodoList) }) {
ChineseText("查看任务列表")
}
// 列表页导航到详情页(带参数)
TodoItem(
todo = todo,
onClick = { onNavigate(AppRoute.TodoDetail(todoId = todo.id)) }
)
// 详情页返回
Button(onClick = onBack) {
ChineseText("返回")
}// 底部导航栏自动管理主要页面
AppBottomNavigation(
currentRoute = currentRoute,
onNavigate = { route ->
// 切换底部导航时清空回退栈
navigationManager.navigateToAndClearStack(route)
}
)// 根据条件决定导航目标
Button(
onClick = {
if (isLoggedIn) {
onNavigate(AppRoute.Profile)
} else {
onNavigate(AppRoute.Login)
}
}
) {
ChineseText("个人中心")
}通过本模块的学习,我对 Compose 应用架构和导航系统有了深入的理解。从类型安全的路由定义到导航管理器的实现,从应用脚手架的设计到页面组件的开发,Compose 提供了一套完整而灵活的应用开发体系。
相比前端框架,Compose 的 sealed class 路由设计提供了更强的类型安全保障,避免了字符串路径带来的运行时错误。导航管理器的实现虽然简单,但清晰地展示了导航的核心概念:当前路由、回退栈、页面跳转和参数传递。应用脚手架的设计则体现了组件化和关注点分离的原则,让代码结构更加清晰。
对于前端开发者来说,理解导航系统是构建完整应用的关键一步。虽然本模块实现的是简化版的导航系统,但它为理解官方 Navigation Compose 库打下了坚实的基础。
项目仓库:https://github.com/easonxie/learn-jetpack-compose
后续会继续更新基于这个项目的更多内容,敬请期待!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。