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 依赖了 OrderRepository 和 AnalyticsTracker。这两个东西怎么来的?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 上。
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:
- OrderInfoFragment:填写订单信息
- OrderPreviewFragment:预览订单
- 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 会自动清理。
Koin 与 Navigation 的架构图
下面这张图展示了 Koin、Navigation 和 ViewModel 之间的关系:
常见问题与避坑指南
我在项目中遇到过几个典型问题,分享出来给大家参考:
如果你在 Fragment 里用了 by navGraphViewModels(R.id.some_graph),但导航图 ID 写错了,Koin 会创建一个新的 ViewModel 实例。结果就是 Fragment 之间数据不共享。
解决方案:确保所有 Fragment 使用的导航图 ID 完全一致。我建议把导航图 ID 定义在 R.navigation 中,不要硬编码字符串。
有时候 Koin 会报错说找不到某个依赖。这通常是因为 ViewModel 的构造参数没有在 Koin 模块中注册。
解决方案:检查 ViewModel 的所有构造参数,确保它们都在 Koin 模块中注册了。可以用 viewModel { OrderViewModel(get(), get()) } 这种写法,Koin 会自动解析 get() 对应的依赖。
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 了。