在组件化Clean Architecture工程中应用Navigation 3.0

前言

在上一篇文章中,我们深入探讨了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的挑战

在组件化工程中使用导航库面临几个挑战:

  1. 模块间导航:如何在不同模块之间进行导航,而不创建强耦合?
  2. 路由定义:如何定义和管理分散在不同模块中的路由?
  3. 参数传递:如何在模块间安全地传递参数?
  4. 导航状态共享:如何在整个应用中共享导航状态?

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,不依赖复杂的依赖注入框架,以保持代码的可读性和可理解性。我们设计了一个灵活的导航系统,支持模块间导航、深层链接和自适应布局。

这种设计的主要优势包括:

  1. 模块化:每个功能模块负责自己的导航逻辑,减少了模块间的耦合。
  2. 可扩展性:可以轻松添加新的功能模块,而不需要修改现有代码。
  3. 可测试性:导航逻辑被封装在独立的类中,便于单元测试。
  4. 灵活性:可以根据屏幕大小和设备类型调整导航体验。
  5. 可读性:不依赖复杂的依赖注入框架,代码更易于理解。

虽然我们的设计不依赖依赖注入框架,但我们也提供了如何使用Hilt、Koin和KAPT/KSP来进一步优化导航系统的指导。这使得开发者可以根据项目的需求和团队的偏好选择最适合的方法。

随着Android应用变得越来越复杂,一个良好设计的导航系统变得越来越重要。Nav3的灵活性和可组合性使其成为组件化Clean Architecture工程的理想选择。

以上实现方式仅作为抛砖引玉之用,在实际开发中,应结合实际项目情况而灵活运用。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容