前言
在上一篇文章中,我们深入探讨了Android Jetpack Navigation 3.0(Nav3)的优势、使用方法、迁移指南以及实现原理。本文将进一步探讨如何在组件化的Clean Architecture工程中应用Nav3,不依赖复杂的依赖注入框架,以保持代码的可读性和可理解性。
组件化和Clean Architecture是现代Android应用开发中的两个重要概念。组件化允许我们将应用拆分为独立的模块,而Clean Architecture则提供了一种将业务逻辑与框架分离的方法。结合Nav3的灵活性,我们可以构建一个既模块化又易于理解的导航系统。
组件化Clean Architecture工程结构
在开始之前,让我们先了解一下组件化Clean Architecture工程的基本结构:
├── app # 应用主模块
├── core # 核心模块(共享代码)
│ ├── common # 通用工具和扩展
│ ├── ui # UI组件和主题
│ └── navigation # 导航相关代码
├── data # 数据层模块
│ ├── repository # 仓库实现
│ ├── local # 本地数据源
│ └── remote # 远程数据源
├── domain # 领域层模块
│ ├── model # 领域模型
│ ├── repository # 仓库接口
│ └── usecase # 用例
└── features # 功能模块
├── feature1 # 功能1
│ ├── ui # 功能1的UI
│ └── domain # 功能1的领域逻辑
├── feature2 # 功能2
└── feature3 # 功能3
在这种结构中,每个功能模块都是独立的,可以单独开发和测试。现在,我们将探讨如何在这种结构中集成Nav3。
在组件化工程中使用Nav3的挑战
在组件化工程中使用导航库面临几个挑战:
- 模块间导航:如何在不同模块之间进行导航,而不创建强耦合?
- 路由定义:如何定义和管理分散在不同模块中的路由?
- 参数传递:如何在模块间安全地传递参数?
- 导航状态共享:如何在整个应用中共享导航状态?
Nav3的设计原则(开发者拥有回退栈、不妨碍开发者、模块化支持)使其非常适合解决这些挑战。
核心导航模块设计
首先,我们需要在core/navigation模块中创建一些基础组件:
1. 定义基础目的地接口
// core/navigation/src/main/kotlin/com/example/core/navigation/Destination.kt
package com.example.core.navigation
/**
* 表示应用中的一个导航目的地
*/
interface Destination {
/**
* 目的地的唯一路由
*/
val route: String
/**
* 目的地的参数
*/
val arguments: Map<String, Any>
get() = emptyMap()
}
2. 创建导航控制器
// core/navigation/src/main/kotlin/com/example/core/navigation/Navigator.kt
package com.example.core.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
/**
* 导航控制器,负责管理应用的导航状态
*/
class Navigator {
/**
* 回退栈,存储当前的导航路径
*/
private val _backStack = mutableListOf<Destination>().toMutableStateList()
/**
* 只读的回退栈
*/
val backStack: SnapshotStateList<Destination> = _backStack
/**
* 当前目的地,如果回退栈为空则返回null
*/
val currentDestination: Destination?
get() = _backStack.lastOrNull()
/**
* 导航到指定目的地
*/
fun navigateTo(destination: Destination) {
_backStack.add(destination)
}
/**
* 返回上一个目的地
* @return 如果成功返回则为true,如果回退栈已空则为false
*/
fun navigateBack(): Boolean {
if (_backStack.size <= 1) return false
_backStack.removeLast()
return true
}
/**
* 返回到根目的地
*/
fun navigateToRoot() {
while (_backStack.size > 1) {
_backStack.removeLast()
}
}
/**
* 替换当前目的地
*/
fun replaceCurrent(destination: Destination) {
if (_backStack.isNotEmpty()) {
_backStack.removeLast()
}
_backStack.add(destination)
}
/**
* 清除回退栈并设置新的根目的地
*/
fun setNewRoot(destination: Destination) {
_backStack.clear()
_backStack.add(destination)
}
}
/**
* 创建并记住一个Navigator实例
*/
@Composable
fun rememberNavigator(initialDestination: Destination? = null): Navigator {
val navigator = remember { Navigator() }
// 如果提供了初始目的地且回退栈为空,则设置初始目的地
if (initialDestination != null && navigator.backStack.isEmpty()) {
navigator.setNewRoot(initialDestination)
}
return navigator
}
3. 创建导航宿主
// core/navigation/src/main/kotlin/com/example/core/navigation/NavigationHost.kt
package com.example.core.navigation
import androidx.compose.runtime.Composable
/**
* 导航宿主,负责根据当前目的地显示相应的内容
*/
@Composable
fun NavigationHost(
navigator: Navigator,
destinationContent: @Composable (Destination) -> Unit
) {
val currentDestination = navigator.currentDestination
if (currentDestination != null) {
destinationContent(currentDestination)
}
}
功能模块中的导航实现
现在,我们将探讨如何在各个功能模块中实现导航。
1. 定义模块特定的目的地
每个功能模块都定义自己的目的地:
// features/feature1/ui/src/main/kotlin/com/example/feature1/ui/navigation/Feature1Destinations.kt
package com.example.feature1.ui.navigation
import com.example.core.navigation.Destination
/**
* 功能1模块的目的地
*/
sealed class Feature1Destination : Destination {
/**
* 功能1的主屏幕
*/
object Main : Feature1Destination() {
override val route: String = "feature1/main"
}
/**
* 功能1的详情屏幕
*/
data class Detail(val itemId: String) : Feature1Destination() {
override val route: String = "feature1/detail"
override val arguments: Map<String, Any>
get() = mapOf("itemId" to itemId)
}
}
2. 创建模块内导航图
// features/feature1/ui/src/main/kotlin/com/example/feature1/ui/navigation/Feature1Navigation.kt
package com.example.feature1.ui.navigation
import androidx.compose.runtime.Composable
import com.example.core.navigation.Destination
import com.example.core.navigation.Navigator
import com.example.feature1.ui.screens.Feature1DetailScreen
import com.example.feature1.ui.screens.Feature1MainScreen
/**
* 功能1模块的导航图
*/
@Composable
fun Feature1Navigation(
destination: Destination,
navigator: Navigator
) {
when (destination) {
is Feature1Destination.Main -> {
Feature1MainScreen(
onItemClick = { itemId ->
navigator.navigateTo(Feature1Destination.Detail(itemId))
}
)
}
is Feature1Destination.Detail -> {
val itemId = destination.arguments["itemId"] as String
Feature1DetailScreen(
itemId = itemId,
onBackClick = {
navigator.navigateBack()
}
)
}
}
}
3. 创建模块导航注册器
为了避免在主模块中硬编码所有功能模块的导航逻辑,我们可以创建一个导航注册器接口:
// core/navigation/src/main/kotlin/com/example/core/navigation/NavigationRegistry.kt
package com.example.core.navigation
import androidx.compose.runtime.Composable
/**
* 导航注册器接口,用于注册模块的导航逻辑
*/
interface NavigationRegistry {
/**
* 检查此注册器是否可以处理给定的目的地
*/
fun canHandle(destination: Destination): Boolean
/**
* 处理导航到给定目的地
*/
@Composable
fun HandleNavigation(destination: Destination, navigator: Navigator)
}
然后,每个功能模块实现这个接口:
// features/feature1/ui/src/main/kotlin/com/example/feature1/ui/navigation/Feature1NavigationRegistry.kt
package com.example.feature1.ui.navigation
import androidx.compose.runtime.Composable
import com.example.core.navigation.Destination
import com.example.core.navigation.NavigationRegistry
import com.example.core.navigation.Navigator
/**
* 功能1模块的导航注册器
*/
class Feature1NavigationRegistry : NavigationRegistry {
override fun canHandle(destination: Destination): Boolean {
return destination is Feature1Destination
}
@Composable
override fun HandleNavigation(destination: Destination, navigator: Navigator) {
Feature1Navigation(destination, navigator)
}
}
在主模块中集成所有导航
最后,在主模块中,我们需要集成所有功能模块的导航:
1. 创建应用级导航宿主
// app/src/main/kotlin/com/example/app/navigation/AppNavigationHost.kt
package com.example.app.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.example.core.navigation.NavigationHost
import com.example.core.navigation.NavigationRegistry
import com.example.core.navigation.Navigator
import com.example.feature1.ui.navigation.Feature1NavigationRegistry
import com.example.feature2.ui.navigation.Feature2NavigationRegistry
import com.example.feature3.ui.navigation.Feature3NavigationRegistry
/**
* 应用级导航宿主,集成所有功能模块的导航
*/
@Composable
fun AppNavigationHost(navigator: Navigator) {
// 收集所有模块的导航注册器
val registries = remember {
listOf(
Feature1NavigationRegistry(),
Feature2NavigationRegistry(),
Feature3NavigationRegistry()
)
}
NavigationHost(navigator) { destination ->
// 查找能处理当前目的地的注册器
val registry = registries.find { it.canHandle(destination) }
if (registry != null) {
registry.HandleNavigation(destination, navigator)
} else {
// 处理未知目的地
UnknownDestinationScreen(destination)
}
}
}
/**
* 显示未知目的地的屏幕
*/
@Composable
private fun UnknownDestinationScreen(destination: com.example.core.navigation.Destination) {
// 显示错误信息或回退到默认屏幕
}
2. 在主活动中设置导航
// app/src/main/kotlin/com/example/app/MainActivity.kt
package com.example.app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import com.example.app.navigation.AppNavigationHost
import com.example.core.navigation.rememberNavigator
import com.example.feature1.ui.navigation.Feature1Destination
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
AppNavigation()
}
}
}
}
@Composable
fun AppNavigation() {
// 创建导航器并设置初始目的地
val navigator = rememberNavigator(initialDestination = Feature1Destination.Main)
// 处理系统返回按钮
val lifecycleOwner = LocalLifecycleOwner.current
val backPressedDispatcher = remember { androidx.activity.OnBackPressedDispatcher() }
LaunchedEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
// 注册返回处理
backPressedDispatcher.addCallback(lifecycleOwner) {
if (!navigator.navigateBack()) {
// 如果导航器无法处理返回,则移除回调并让系统处理
remove()
backPressedDispatcher.onBackPressed()
}
}
}
}
lifecycleOwner.lifecycle.addObserver(observer)
}
// 设置导航宿主
AppNavigationHost(navigator)
}
深层链接和外部导航
在组件化应用中,处理深层链接和外部导航也是一个重要的考虑因素。我们可以扩展我们的导航系统来支持这些功能:
1. 创建深层链接解析器
// core/navigation/src/main/kotlin/com/example/core/navigation/DeepLinkParser.kt
package com.example.core.navigation
/**
* 深层链接解析器接口
*/
interface DeepLinkParser {
/**
* 检查此解析器是否可以处理给定的URI
*/
fun canParse(uri: String): Boolean
/**
* 将URI解析为目的地
*/
fun parse(uri: String): Destination?
}
2. 在功能模块中实现深层链接解析
// features/feature1/ui/src/main/kotlin/com/example/feature1/ui/navigation/Feature1DeepLinkParser.kt
package com.example.feature1.ui.navigation
import com.example.core.navigation.DeepLinkParser
import com.example.core.navigation.Destination
/**
* 功能1模块的深层链接解析器
*/
class Feature1DeepLinkParser : DeepLinkParser {
override fun canParse(uri: String): Boolean {
return uri.startsWith("myapp://feature1")
}
override fun parse(uri: String): Destination? {
return when {
uri == "myapp://feature1/main" -> Feature1Destination.Main
uri.startsWith("myapp://feature1/detail/") -> {
val itemId = uri.substringAfterLast("/")
Feature1Destination.Detail(itemId)
}
else -> null
}
}
}
3. 在主模块中处理深层链接
// app/src/main/kotlin/com/example/app/deeplink/AppDeepLinkHandler.kt
package com.example.app.deeplink
import com.example.core.navigation.DeepLinkParser
import com.example.core.navigation.Destination
import com.example.core.navigation.Navigator
import com.example.feature1.ui.navigation.Feature1DeepLinkParser
import com.example.feature2.ui.navigation.Feature2DeepLinkParser
import com.example.feature3.ui.navigation.Feature3DeepLinkParser
/**
* 应用级深层链接处理器
*/
class AppDeepLinkHandler(private val navigator: Navigator) {
private val parsers = listOf(
Feature1DeepLinkParser(),
Feature2DeepLinkParser(),
Feature3DeepLinkParser()
)
/**
* 处理深层链接URI
* @return 如果成功处理则为true,否则为false
*/
fun handleDeepLink(uri: String): Boolean {
// 查找能解析此URI的解析器
val parser = parsers.find { it.canParse(uri) }
// 解析URI为目的地
val destination = parser?.parse(uri)
return if (destination != null) {
// 导航到解析出的目的地
navigator.navigateTo(destination)
true
} else {
false
}
}
}
然后在MainActivity中处理传入的意图:
// app/src/main/kotlin/com/example/app/MainActivity.kt (更新)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
val navigator = rememberNavigator(initialDestination = Feature1Destination.Main)
val deepLinkHandler = remember { AppDeepLinkHandler(navigator) }
// 处理传入的意图
LaunchedEffect(intent) {
intent?.data?.toString()?.let { uri ->
deepLinkHandler.handleDeepLink(uri)
}
}
AppNavigation(navigator)
}
}
}
适应性布局和多窗格导航
Nav3的一个主要优势是能够轻松实现自适应布局。在组件化工程中,我们可以扩展我们的导航系统来支持多窗格布局:
// app/src/main/kotlin/com/example/app/navigation/AdaptiveNavigationHost.kt
package com.example.app.navigation
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.example.core.navigation.NavigationRegistry
import com.example.core.navigation.Navigator
import com.example.feature1.ui.navigation.Feature1Destination
import com.example.feature1.ui.navigation.Feature1NavigationRegistry
import com.example.feature2.ui.navigation.Feature2NavigationRegistry
import com.example.feature3.ui.navigation.Feature3NavigationRegistry
/**
* 自适应导航宿主,根据屏幕大小选择不同的布局
*/
@Composable
fun AdaptiveNavigationHost(
navigator: Navigator,
windowSizeClass: WindowSizeClass
) {
// 收集所有模块的导航注册器
val registries = remember {
listOf(
Feature1NavigationRegistry(),
Feature2NavigationRegistry(),
Feature3NavigationRegistry()
)
}
// 根据屏幕宽度选择布局
if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {
// 大屏幕:双窗格布局
TwoPaneLayout(navigator, registries)
} else {
// 小屏幕:单窗格布局
SinglePaneLayout(navigator, registries)
}
}
/**
* 单窗格布局
*/
@Composable
private fun SinglePaneLayout(
navigator: Navigator,
registries: List<NavigationRegistry>
) {
val currentDestination = navigator.currentDestination
if (currentDestination != null) {
// 查找能处理当前目的地的注册器
val registry = registries.find { it.canHandle(currentDestination) }
if (registry != null) {
registry.HandleNavigation(currentDestination, navigator)
} else {
// 处理未知目的地
UnknownDestinationScreen(currentDestination)
}
}
}
/**
* 双窗格布局
*/
@Composable
private fun TwoPaneLayout(
navigator: Navigator,
registries: List<NavigationRegistry>
) {
Row {
// 主窗格:始终显示主列表
val mainDestination = remember(navigator.backStack) {
navigator.backStack.firstOrNull() ?: Feature1Destination.Main
}
// 查找能处理主目的地的注册器
val mainRegistry = registries.find { it.canHandle(mainDestination) }
// 主窗格
androidx.compose.foundation.layout.Box(modifier = Modifier.weight(1f)) {
if (mainRegistry != null) {
mainRegistry.HandleNavigation(mainDestination, navigator)
}
}
// 详情窗格:显示次要内容
val detailDestination = remember(navigator.backStack) {
if (navigator.backStack.size > 1) navigator.backStack.last() else null
}
// 详情窗格
androidx.compose.foundation.layout.Box(modifier = Modifier.weight(2f)) {
if (detailDestination != null) {
// 查找能处理详情目的地的注册器
val detailRegistry = registries.find { it.canHandle(detailDestination) }
if (detailRegistry != null) {
detailRegistry.HandleNavigation(detailDestination, navigator)
}
} else {
// 显示空状态
EmptyDetailScreen()
}
}
}
}
/**
* 显示空详情的屏幕
*/
@Composable
private fun EmptyDetailScreen() {
// 显示选择项目的提示
}
后期优化:添加依赖注入
虽然我们的设计不依赖依赖注入框架,但在实际项目中,依赖注入可以进一步简化代码并提高可测试性。以下是如何使用Hilt或Koin等框架来优化我们的导航系统:
使用Hilt优化
// 添加Hilt模块来提供导航注册器
@Module
@InstallIn(SingletonComponent::class)
abstract class NavigationModule {
@Binds
@IntoSet
abstract fun bindFeature1NavigationRegistry(impl: Feature1NavigationRegistry): NavigationRegistry
@Binds
@IntoSet
abstract fun bindFeature2NavigationRegistry(impl: Feature2NavigationRegistry): NavigationRegistry
@Binds
@IntoSet
abstract fun bindFeature3NavigationRegistry(impl: Feature3NavigationRegistry): NavigationRegistry
}
// 在AppNavigationHost中注入注册器集合
@Composable
fun AppNavigationHost(
navigator: Navigator,
@Inject registries: Set<@JvmSuppressWildcards NavigationRegistry>
) {
NavigationHost(navigator) { destination ->
// 查找能处理当前目的地的注册器
val registry = registries.find { it.canHandle(destination) }
if (registry != null) {
registry.HandleNavigation(destination, navigator)
} else {
// 处理未知目的地
UnknownDestinationScreen(destination)
}
}
}
使用Koin优化
// 定义Koin模块
val navigationModule = module {
single<NavigationRegistry> { Feature1NavigationRegistry() }
single<NavigationRegistry> { Feature2NavigationRegistry() }
single<NavigationRegistry> { Feature3NavigationRegistry() }
}
// 在AppNavigationHost中获取注册器集合
@Composable
fun AppNavigationHost(navigator: Navigator) {
val registries = getKoin().getAll<NavigationRegistry>()
NavigationHost(navigator) { destination ->
// 查找能处理当前目的地的注册器
val registry = registries.find { it.canHandle(destination) }
if (registry != null) {
registry.HandleNavigation(destination, navigator)
} else {
// 处理未知目的地
UnknownDestinationScreen(destination)
}
}
}
使用KAPT/KSP优化路由注册
另一种优化方法是使用注解处理器自动注册路由,类似于ARouter的方式:
// 定义路由注解
@Retention(AnnotationRetention.BINARY)
@Target(AnnotationTarget.CLASS)
annotation class Route(val path: String)
// 在目的地类上使用注解
@Route("feature1/main")
class Feature1MainScreen : Destination {
override val route: String = "feature1/main"
}
// 使用注解处理器生成路由表
// 这部分需要KAPT/KSP实现,生成类似以下代码:
class GeneratedRouteRegistry {
companion object {
val routes = mapOf(
"feature1/main" to Feature1MainScreen::class.java,
"feature1/detail" to Feature1DetailScreen::class.java,
// ...
)
}
}
结论
在本文中,我们探讨了如何在基于Clean Architecture的组件化工程中应用Nav3,不依赖复杂的依赖注入框架,以保持代码的可读性和可理解性。我们设计了一个灵活的导航系统,支持模块间导航、深层链接和自适应布局。
这种设计的主要优势包括:
- 模块化:每个功能模块负责自己的导航逻辑,减少了模块间的耦合。
- 可扩展性:可以轻松添加新的功能模块,而不需要修改现有代码。
- 可测试性:导航逻辑被封装在独立的类中,便于单元测试。
- 灵活性:可以根据屏幕大小和设备类型调整导航体验。
- 可读性:不依赖复杂的依赖注入框架,代码更易于理解。
虽然我们的设计不依赖依赖注入框架,但我们也提供了如何使用Hilt、Koin和KAPT/KSP来进一步优化导航系统的指导。这使得开发者可以根据项目的需求和团队的偏好选择最适合的方法。
随着Android应用变得越来越复杂,一个良好设计的导航系统变得越来越重要。Nav3的灵活性和可组合性使其成为组件化Clean Architecture工程的理想选择。
以上实现方式仅作为抛砖引玉之用,在实际开发中,应结合实际项目情况而灵活运用。