向ViewModel使用的辅助类传递viewModelScope的最佳实践是什么
向ViewModel辅助类传递协程作用域的最佳实践
首先纠正一个常见设计误区:你的AccountManager不需要持有任何ViewModel的viewModelScope,合理设计Flow的责任边界即可同时满足多场景复用、无内存泄漏的需求,以下是两种对应不同场景的落地方案:
方案1:方法返回冷Flow(绝大多数场景优先推荐)
所有异步任务、数据库/网络请求逻辑都封装在Flow构建器内部,AccountManager不需要持有任何协程作用域,执行时机和生命周期完全由调用方(ViewModel)的viewModelScope控制。
示例代码:
// 可直接声明为Singleton,无任何内存泄漏风险 class AccountManager @Inject constructor( private val accountDao: AccountDao, private val userApi: UserApi ) { // 账户查询逻辑直接返回冷Flow fun fetchAccountInfo(): Flow<Result<Account>> = flow { // 先返回本地缓存数据 val localAccount = accountDao.queryCurrentAccount() emit(Result.success(localAccount)) // 拉取远端数据更新本地后返回 val remoteAccount = userApi.getAccountInfo() accountDao.insert(remoteAccount) emit(Result.success(remoteAccount)) }.catch { emit(Result.failure(it)) }.flowOn(Dispatchers.IO) // 纯异步操作也可直接声明为suspend方法 suspend fun logout(): Result<Unit> = runCatching { userApi.logout() accountDao.clearAll() } }
ViewModel调用示例:
@HiltViewModel class UserViewModel @Inject constructor( private val accountManager: AccountManager ): ViewModel() { init { // 用当前ViewModel的viewModelScope启动收集,ViewModel销毁时自动终止任务 viewModelScope.launch { accountManager.fetchAccountInfo().collect { result -> // 处理返回结果更新UI状态 } } } fun triggerLogout() { viewModelScope.launch { val logoutResult = accountManager.logout() // 处理登出结果 } } }
优势:
- AccountManager和任意ViewModel的生命周期完全解耦,声明为单例也不会持有任何ViewModel的引用,无内存泄漏风险
- 多ViewModel复用逻辑时,各自的异步任务由自身的viewModelScope控制,互不干扰
- 不需要额外管理协程作用域,代码逻辑更简洁
方案2:单例持有热Flow(需要全局推送事件场景使用)
如果需要AccountManager主动向多个观察者推送全局状态(比如账号被踢下线、权限变更等),可以让单例AccountManager自己持有和App生命周期一致的独立协程作用域,用热Flow对外暴露状态,ViewModel只需要用自身的viewModelScope订阅即可。
示例代码:
@Singleton class AccountManager @Inject constructor( private val userApi: UserApi ) { // 单例自身的协程作用域,生命周期和App一致,和ViewModel完全无关 private val managerScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) // 热Flow存储全局账号状态 private val _accountStatus = MutableSharedFlow<AccountStatus>(replay = 1) val accountStatus: SharedFlow<AccountStatus> = _accountStatus.asSharedFlow() // 内部逻辑主动更新状态 fun onTokenExpired() { managerScope.launch { _accountStatus.emit(AccountStatus.TOKEN_EXPIRED) } } }
ViewModel订阅示例:
@HiltViewModel class HomeViewModel @Inject constructor( private val accountManager: AccountManager ): ViewModel() { init { viewModelScope.launch { // 订阅状态,ViewModel销毁时自动取消订阅,不会残留引用 accountManager.accountStatus.collect { status -> if (status == AccountStatus.TOKEN_EXPIRED) { // 跳转到登录页 } } } } }
优势:
- 不需要绑定任何ViewModel的作用域,单例自身的作用域仅跟随App生命周期
- 所有订阅方的生命周期自行管理,AccountManager不需要感知订阅方的存在,不会出现内存泄漏
绝对禁止的做法
不要将ViewModel的viewModelScope传递给生命周期更长的实例(比如Singleton的AccountManager),单例的生命周期和Application一致,持有viewModelScope的引用会直接导致对应的ViewModel无法被GC回收,造成严重内存泄漏。
内容的提问来源于stack exchange,提问作者Dr Android
相关产品推荐
相关产品推荐

