21. Koin与Navigation:Navigation组件中的Koin集成、NavGraphViewModels

Navigation 组件现在几乎是 Android 项目的标配了。但有个问题一直挺让人头疼——Fragment 之间的 ViewModel 共享。特别是当你用 Koin 做依赖注入时,怎么让 Navigation 和 DI 框架配合得天衣无缝?

我最早做 Navigation 项目时,用的是最原始的方式:每个 Fragment 自己从 Koin 里拿 ViewModel。结果呢?代码里到处都是 by viewModel()getViewModel(),而且 Fragment 之间想共享数据还得靠 Activity 级别的 ViewModel。说实话,那体验真不怎么样。

后来 Koin 3.2 版本开始,官方终于正式支持了 NavGraphViewModel。嗯,这玩意儿确实好用。今天我们就来聊聊怎么把 Koin 和 Navigation 组件玩出花来。

为什么需要 NavGraphViewModel?

先想想看,一个典型的 Navigation 场景:你有三个 Fragment——A、B、C,它们属于同一个导航图。A 让用户填写信息,B 展示预览,C 提交结果。这三个 Fragment 需要共享同一个 ViewModel。

传统做法有两种:

  • 把 ViewModel 放在 Activity 级别——但这样 ViewModel 的生命周期就太长了,而且其他不相关的 Fragment 也能访问到它
  • by navGraphViewModels()——但这样 ViewModel 的构造依赖需要手动处理,Koin 没法自动注入

Koin 的 NavGraphViewModel 支持,说白了就是解决了这个痛点。它让 ViewModel 既能限定在导航图的作用域内,又能享受 Koin 的自动注入能力。

集成步骤

首先,你得在 build.gradle 里加上 Koin 的 Navigation 扩展库:

// 如果你用 Koin 3.x
implementation "io.insert-koin:koin-androidx-navigation:3.5.0"

// 如果你用 Koin 2.x(不推荐,但老项目可能还在用)
implementation "org.koin:koin-androidx-navigation:2.2.3"

注意版本号要和你项目里的 Koin 主版本一致。我见过有人 Koin 主库是 3.4.0,但 navigation 扩展库是 3.2.0,结果运行时直接崩了。这种低级错误,排查起来还挺费时间的。

定义 ViewModel

接下来,定义你的 ViewModel。这里有个关键点:ViewModel 的构造函数参数,Koin 会自动帮你注入。

class OrderViewModel(
    private val orderRepository: OrderRepository,
    private val analyticsTracker: AnalyticsTracker
) : ViewModel() {

    private val _orderState = MutableStateFlow<OrderState>(OrderState.Empty)
    val orderState: StateFlow<OrderState> = _orderState.asStateFlow()

    fun updateOrderInfo(info: OrderInfo) {
        viewModelScope.launch {
            _orderState.value = OrderState.Loading
            val result = orderRepository.saveOrder(info)
            analyticsTracker.track("order_updated", mapOf("order_id" to info.id))
            _orderState.value = OrderState.Success(result)
        }
    }
}

你看,这个 ViewModel 依赖了 OrderRepositoryAnalyticsTracker。这两个东西怎么来的?Koin 会在创建 ViewModel 时自动从容器里拿。

在 Koin 模块中注册

然后,在 Koin 模块里注册这个 ViewModel:

val orderModule = module {
    // 普通的单例或工厂
    single { OrderRepository(get()) }
    single { AnalyticsTracker(get()) }

    // ViewModel 的注册方式
    viewModel { OrderViewModel(get(), get()) }
}

这里用 viewModel { } 而不是 single { }factory { }。为什么?因为 ViewModel 的生命周期是由 Android 框架管理的,Koin 只是负责创建它。用 viewModel 关键字,Koin 会知道这个实例应该绑定到 ViewModelStoreOwner 上。

小提示:如果你在同一个导航图里有多个 Fragment 需要共享 ViewModel,记得用 viewModel 而不是 scoped。我刚开始时搞混过,结果每个 Fragment 都拿到了不同的 ViewModel 实例,数据根本共享不了。

在 Fragment 中使用

好了,重头戏来了。在 Fragment 里怎么拿到这个 ViewModel?

class OrderInfoFragment : Fragment() {

    // 传统方式:by viewModel() —— 这是 Fragment 级别的,不共享
    // private val viewModel: OrderViewModel by viewModel()

    // Koin + Navigation 方式:by navGraphViewModels()
    private val viewModel: OrderViewModel by navGraphViewModels(R.id.order_nav_graph)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 现在可以直接用 viewModel 了
        viewModel.orderState.collect { state ->
            when (state) {
                is OrderState.Empty -> showEmptyState()
                is OrderState.Loading -> showLoading()
                is OrderState.Success -> showSuccess(state.data)
            }
        }
    }
}

注意看 R.id.order_nav_graph 这个参数。它指定了 ViewModel 的作用域——所有属于 order_nav_graph 这个导航图的 Fragment,拿到的都是同一个 ViewModel 实例。

我曾经在项目里踩过一个坑:导航图 ID 写错了,结果 ViewModel 的作用域变成了 Activity 级别。数据倒是能共享了,但 Fragment 销毁重建后 ViewModel 还活着,导致一些状态没重置。嗯,这种 bug 特别隐蔽,测试都不一定能测出来。

NavGraphViewModel 的完整示例

我们来写一个完整的例子。假设有一个订单流程,包含三个 Fragment:

  1. OrderInfoFragment:填写订单信息
  2. OrderPreviewFragment:预览订单
  3. OrderSubmitFragment:提交订单

它们共享同一个 OrderViewModel

首先,定义导航图(nav_graph.xml):

<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/order_nav_graph"
    app:startDestination="@id/orderInfoFragment">

    <fragment
        android:id="@+id/orderInfoFragment"
        android:name="com.example.app.OrderInfoFragment"
        android:label="订单信息"
        tools:layout="@layout/fragment_order_info">
        <action
            android:id="@+id/action_orderInfo_to_orderPreview"
            app:destination="@id/orderPreviewFragment" />
    </fragment>

    <fragment
        android:id="@+id/orderPreviewFragment"
        android:name="com.example.app.OrderPreviewFragment"
        android:label="订单预览"
        tools:layout="@layout/fragment_order_preview">
        <action
            android:id="@+id/action_orderPreview_to_orderSubmit"
            app:destination="@id/orderSubmitFragment" />
    </fragment>

    <fragment
        android:id="@+id/orderSubmitFragment"
        android:name="com.example.app.OrderSubmitFragment"
        android:label="提交订单"
        tools:layout="@layout/fragment_order_submit" />
</navigation>

然后,在三个 Fragment 中都用 by navGraphViewModels(R.id.order_nav_graph) 来获取 ViewModel:

// OrderInfoFragment.kt
class OrderInfoFragment : Fragment() {
    private val viewModel: OrderViewModel by navGraphViewModels(R.id.order_nav_graph)

    fun onSaveInfo(name: String, phone: String) {
        viewModel.updateOrderInfo(OrderInfo(name = name, phone = phone))
        findNavController().navigate(R.id.action_orderInfo_to_orderPreview)
    }
}

// OrderPreviewFragment.kt
class OrderPreviewFragment : Fragment() {
    private val viewModel: OrderViewModel by navGraphViewModels(R.id.order_nav_graph)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 直接读取 viewModel 中的数据,不需要额外传参
        viewModel.orderState.collect { state ->
            // 显示预览
        }
    }
}

// OrderSubmitFragment.kt
class OrderSubmitFragment : Fragment() {
    private val viewModel: OrderViewModel by navGraphViewModels(R.id.order_nav_graph)

    fun onSubmit() {
        viewModel.submitOrder()
    }
}

你看,三个 Fragment 之间不需要通过 Bundle 传数据,也不需要把 ViewModel 提升到 Activity 级别。它们共享同一个 ViewModel,而且这个 ViewModel 的生命周期和导航图绑定——当导航图被弹出(pop)时,ViewModel 会自动清理。

核心要点:NavGraphViewModel 的生命周期 = 导航图的生命周期。导航图被创建时 ViewModel 被创建,导航图被销毁时 ViewModel 被清理。这比 Activity 级别的 ViewModel 更精确,比 Fragment 级别的 ViewModel 更灵活。

Koin 与 Navigation 的架构图

下面这张图展示了 Koin、Navigation 和 ViewModel 之间的关系:

Koin + Navigation 架构图 Koin 容器 Repository 模块 OrderRepository Analytics 模块 AnalyticsTracker ViewModel 模块 OrderViewModel Navigation 导航图 (order_nav_graph) OrderInfoFragment 填写订单信息 by navGraphViewModels() OrderPreviewFragment 预览订单 by navGraphViewModels() OrderSubmitFragment 提交订单 by navGraphViewModels() 共享 ViewModel OrderViewModel 生命周期 = 导航图 ViewModel 共享 依赖注入

常见问题与避坑指南

我在项目中遇到过几个典型问题,分享出来给大家参考:

问题一:ViewModel 重复创建

如果你在 Fragment 里用了 by navGraphViewModels(R.id.some_graph),但导航图 ID 写错了,Koin 会创建一个新的 ViewModel 实例。结果就是 Fragment 之间数据不共享。

解决方案:确保所有 Fragment 使用的导航图 ID 完全一致。我建议把导航图 ID 定义在 R.navigation 中,不要硬编码字符串。

问题二:ViewModel 构造参数注入失败

有时候 Koin 会报错说找不到某个依赖。这通常是因为 ViewModel 的构造参数没有在 Koin 模块中注册。

解决方案:检查 ViewModel 的所有构造参数,确保它们都在 Koin 模块中注册了。可以用 viewModel { OrderViewModel(get(), get()) } 这种写法,Koin 会自动解析 get() 对应的依赖。

个人经验:我曾经在一个大型项目中,导航图嵌套了三级。最内层的 Fragment 想访问外层导航图的 ViewModel,结果怎么都拿不到。后来发现 navGraphViewModels() 只能访问当前 Fragment 所属的直接导航图,不能跨级访问。解决方案是把共享数据提升到父导航图,或者用 SavedStateHandle 来传递。

性能与最佳实践

最后,说几个我总结的最佳实践:

  • 导航图不宜过大:一个导航图里放太多 Fragment,ViewModel 的职责会变得模糊。我一般控制在 3-5 个 Fragment 以内。
  • ViewModel 职责单一:共享 ViewModel 只放共享数据,不要把所有逻辑都塞进去。每个 Fragment 特有的逻辑,放在各自的 ViewModel 里。
  • 避免在 ViewModel 中持有 Fragment 引用:这是老生常谈了,但用 Navigation 时特别容易犯。ViewModel 里不要传 Context 或 Fragment 的引用,用 SavedStateHandle 代替。
  • 测试时注意作用域:单元测试中,navGraphViewModels() 需要 Navigation 环境。可以用 TestNavHostController 来模拟。

嗯,关于 Koin 和 Navigation 的集成,今天就聊这么多。说白了,核心就是 by navGraphViewModels() 这个扩展函数,加上 Koin 的 viewModel { } 注册方式。掌握了这两点,你就能在 Navigation 组件中优雅地使用 Koin 了。