第19章:Koin与ViewModel:viewModel DSL、ViewModel注入的最佳实践
ViewModel 的依赖注入,说白了就是解决一个问题:ViewModel 怎么拿到它需要的那些依赖?
我刚开始用 Koin 的时候,觉得这玩意儿跟注入普通对象没啥区别。后来踩了几个坑才明白——ViewModel 的生命周期跟 Activity/Fragment 绑定,它的创建方式也跟普通类不一样。嗯,这里要注意的东西其实不少。
19.1 为什么 ViewModel 的注入需要特殊处理?
你想想看,普通的类我们直接用 get() 或者构造函数注入就行。但 ViewModel 呢?它是由 ViewModelProvider 管理的,系统负责创建和缓存。如果我们自己 new 一个 ViewModel,那配置旋转时数据就丢了——这显然不行。
所以 Koin 提供了专门的 viewModel DSL,让你既能享受依赖注入的便利,又不破坏 ViewModel 的生命周期管理。
核心区别:
single { }→ 全局单例,跟 Activity 生命周期无关factory { }→ 每次注入都创建新实例viewModel { }→ 由 ViewModelProvider 管理,生命周期感知
19.2 viewModel DSL 的基本用法
先看一个最简单的例子。假设我们有一个 UserViewModel,它依赖 UserRepository:
// 1. 定义模块
val appModule = module {
single { UserRepository() }
viewModel { UserViewModel(get()) }
}
// 2. 在 Activity 中注入
class MainActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 直接用,不用管创建和缓存
viewModel.loadUser()
}
}
这里 by viewModel() 是 Koin 提供的委托属性。它背后调用了 ViewModelProvider,所以 ViewModel 的缓存机制完全不受影响。
个人习惯:我一般把 ViewModel 的声明放在 Activity/Fragment 的顶部,用 private val 修饰。这样整个类都能访问,而且生命周期清晰。
19.3 带参数的 ViewModel 注入
实际项目中,ViewModel 经常需要接收一些运行时参数。比如用户 ID、订单 ID 之类的。Koin 也支持这种情况:
// 定义带参数的 ViewModel
class OrderViewModel(
private val orderId: String,
private val orderRepository: OrderRepository
) : ViewModel() {
// ...
}
// 在模块中声明
val appModule = module {
single { OrderRepository() }
viewModel { params ->
OrderViewModel(
orderId = params.get(),
orderRepository = get()
)
}
}
// 在 Activity 中传参
class OrderActivity : AppCompatActivity() {
private val viewModel: OrderViewModel by viewModel(
parameters = { parametersOf(intent.getStringExtra("order_id")) }
)
}
这里有个细节:params.get() 默认返回的是 Any? 类型,你需要确保传参的类型匹配。我曾经在这里翻过车——传了个 Int 进去,但 ViewModel 期望的是 String,结果运行时直接崩溃。
避坑指南:参数类型一定要严格匹配。Koin 不会帮你做隐式转换。建议在传参时显式指定类型,比如 parametersOf("order_123") 而不是 parametersOf(123)。
19.4 ViewModel 注入的最佳实践
用了这么多年 Koin,我总结了几条比较实用的经验:
19.4.1 保持 ViewModel 的构造函数简洁
ViewModel 的构造函数里只放它真正需要的依赖。别把整个 Repository 层都塞进去。我见过有人把 7、8 个参数塞进 ViewModel 的构造函数——那代码简直没法维护。
// ❌ 不推荐
class DashboardViewModel(
private val userRepo: UserRepository,
private val orderRepo: OrderRepository,
private val productRepo: ProductRepository,
private val analyticsRepo: AnalyticsRepository,
private val configRepo: ConfigRepository
) : ViewModel()
// ✅ 推荐:只放必要的
class DashboardViewModel(
private val userRepo: UserRepository,
private val orderRepo: OrderRepository
) : ViewModel()
19.4.2 使用 SavedStateHandle 保存状态
如果 ViewModel 需要保存 UI 状态(比如表单输入),建议用 SavedStateHandle。Koin 也支持:
class FormViewModel(
private val savedStateHandle: SavedStateHandle,
private val formRepository: FormRepository
) : ViewModel() {
val userName = savedStateHandle.getLiveData<String>("user_name", "")
fun saveName(name: String) {
savedStateHandle["user_name"] = name
}
}
// 模块声明
val appModule = module {
single { FormRepository() }
viewModel { FormViewModel(get(), get()) }
}
注意:SavedStateHandle 不需要你手动传参,Koin 会自动注入。但前提是 ViewModel 必须通过 viewModel DSL 声明。
19.4.3 避免在 ViewModel 中持有 Activity 引用
这个其实是个老生常谈的问题。但用 Koin 的时候更容易犯——因为注入太方便了,一不小心就把 Context 传进去了。
我的建议:如果 ViewModel 确实需要 Context,用 AndroidViewModel 而不是直接传 Context。Koin 对 AndroidViewModel 的支持也是开箱即用的:
class MyViewModel(application: Application) : AndroidViewModel(application) {
// 可以通过 getApplication() 获取 Context
}
// 模块声明
viewModel { MyViewModel(get()) }
19.5 多模块项目中的 ViewModel 注入
项目大了以后,ViewModel 可能分散在不同的模块里。比如 feature:login、feature:home 各自有自己的 ViewModel。这时候怎么组织模块?
我习惯的做法是:每个 feature 模块自己声明自己的 ViewModel 模块,然后在主模块里统一加载。
// feature/login 模块
val loginModule = module {
viewModel { LoginViewModel(get()) }
}
// feature/home 模块
val homeModule = module {
viewModel { HomeViewModel(get()) }
}
// app 主模块
startKoin {
modules(
loginModule,
homeModule,
// ... 其他模块
)
}
这样做的好处是:每个模块的依赖关系清晰,不会出现跨模块的循环依赖。而且测试的时候可以只加载需要的模块。
19.6 测试时的 ViewModel 注入
测试 ViewModel 的时候,Koin 反而帮不上太多忙——因为测试环境里我们通常直接 new 出来,而不是通过 Koin 容器获取。
class UserViewModelTest {
private lateinit var viewModel: UserViewModel
private val mockRepo = mock<UserRepository>()
@Before
fun setup() {
// 直接 new,不经过 Koin
viewModel = UserViewModel(mockRepo)
}
@Test
fun `test load user`() {
// 测试逻辑
}
}
但如果你用的是 AndroidViewModel 或者需要 SavedStateHandle,那就得稍微麻烦一点:
class FormViewModelTest {
private lateinit var viewModel: FormViewModel
private val mockRepo = mock<FormRepository>()
@Before
fun setup() {
// 创建 SavedStateHandle 的模拟实例
val handle = SavedStateHandle(mapOf("user_name" to "test"))
viewModel = FormViewModel(handle, mockRepo)
}
}
小技巧:如果 ViewModel 的构造函数参数太多,可以考虑用 @Inject 注解配合 Dagger Hilt 来测试。但既然我们在讲 Koin,那就保持简单——直接 new 是最清晰的。
19.7 本章小结
ViewModel 的注入,说白了就是两件事:
- 用
viewModel DSL声明,而不是single或factory - 在 Activity/Fragment 里用
by viewModel()获取
记住这两点,大部分场景都不会出问题。至于参数传递、状态保存、多模块组织这些,都是锦上添花的东西。先把基础打牢,再慢慢优化。
我个人觉得,Koin 在 ViewModel 注入这块做得比 Dagger 更直观。没有那么多注解,没有那么多模板代码。你只要记住 ViewModel 的生命周期是跟 Activity 绑定的,别把它当成普通单例来用就行。