第3章 跨模块ViewModel共享方案

3.1 为什么需要跨模块共享 ViewModel

在模块化架构中,不同模块的 Fragment 或 Activity 经常需要共享同一份界面状态或业务数据。例如:商品详情模块与购物车模块都需要监听同一购物车计数;用户模块与订单模块都需要读取登录态。若每个模块各自创建 ViewModel,数据将无法同步,也无法实现跨模块通信。

Android 的 ViewModel 本身支持按作用域(Scope)共享——同一个 ViewModelStoreOwner 内通过相同 key 创建的 ViewModel 实例唯一。因此关键抉择在于:选择 Activity 作为作用域(ActivityScope)还是选择父 Fragment 作为作用域(FragmentScope)。本章节将解析两种作用域的差异、实现流程及模块化落地细节。

3.2 作用域原理

ViewModel 实例由 ViewModelProvider 管理,其内部通过 ViewModelStore 缓存实例。生命周期由 ViewModelStoreOwner(Activity 或 Fragment)决定:

  • ActivityScope:ViewModel 与 Activity 生命周期绑定,Activity 销毁时(非 configuration change)ViewModel 才销毁。所有挂载在该 Activity 下的 Fragment 均可共享同一个 ViewModel 实例。
  • FragmentScope:ViewModel 与父 Fragment 生命周期绑定,父 Fragment 销毁时 ViewModel 销毁。仅在该父 Fragment 及其子 Fragment 间共享。

核心 API 为 ViewModelProvider(storeOwner),其中 storeOwner 决定了作用域。

维度ActivityScopeFragmentScope
Owner 类型ActivityFragment
生命周期与 Activity 一致与父 Fragment 一致
共享范围Activity 内所有 Fragment父 Fragment 及其子 Fragment
内存释放Activity 销毁时父 Fragment 销毁时
模块化耦合各模块需依赖公共 Activity各模块需依赖公共父 Fragment
适用场景跨多个 Fragment 页面的全局状态(如购物车、登录态)仅在一个复合 Fragment 内部的子页面间共享数据(如多步骤表单)
最佳实践:对于跨模块共享,优先使用 ActivityScope 以保证足够的作用域范围;若业务明确限定在单个 Feature 内部,使用 FragmentScope 可减少内存驻留。

3.3 实现流程

  1. 定义 SharedViewModel:将 ViewModel 类放在公共模块(如 common 或 shared 模块),使其可被多个模块依赖。
  2. 决定作用域类型:
    • ActivityScope:在 Activity 中创建 ViewModel,Fragment 通过 requireActivity().viewModels<SharedVM>() 获取。
    • FragmentScope:在父 Fragment 中创建 ViewModel,子 Fragment 通过 requireParentFragment().viewModels<SharedVM>() 获取。
  3. 使用 Hilt 注入(推荐):通过 @HiltViewModel 和 hiltViewModel() 扩展函数自动关联作用域。
  4. 数据通信:使用 LiveData 或 StateFlow 暴露状态,各 Fragment 仅做观察。
// 公共模块中的 SharedViewModel.kt
class CartViewModel @Inject constructor(
    private val cartRepository: CartRepository
) : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun addToCart(productId: String) {
        viewModelScope.launch {
            cartRepository.addItem(productId)
            _count.value = cartRepository.getItemCount()
        }
    }
}

// ActivityScope 获取(在 DetailFragment 中)
class DetailFragment : Fragment() {
    private val cartViewModel: CartViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // 直接使用 cartViewModel
        viewLifecycleOwner.lifecycleScope.launch {
            cartViewModel.count.collect { count ->
                // 更新 UI
            }
        }
    }
}

// FragmentScope 获取(在 ChildFragment 中)
class ChildFragment : Fragment() {
    private val parentViewModel: CartViewModel? by parentViewModels() // 父 Fragment 需先创建
}

// Hilt 简化版(Activity/Fragment 内)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val cartViewModel: CartViewModel by viewModels() // 默认 ActivityScope
}

@AndroidEntryPoint
class DetailFragment : Fragment() {
    private val cartViewModel: CartViewModel by hiltViewModel() // 也默认 ActivityScope
}

3.4 作用域选择流程图

下图展示了 ActivityScope 与 FragmentScope 在模块化布局中的共享区别。
跨模块 ViewModel 共享作用域模型 Activity(作用域 Owner) FragmentA(模块 A) FragmentB(模块 B) SharedViewModel(ActivityScope) FragmentScope 替代 将 SharedViewModel 绑定到父 Fragment 仅在该父 Fragment 内共享 选择依据 ActivityScope → 跨模块全局 FragmentScope → 模块内局部

3.5 常见错误与调试

错误现象原因解决方案
作用域不匹配Fragment 获取 ViewModel 时返回新实例,状态不共享使用了 by viewModels()(默认 Fragment 作用域)而不是 by activityViewModels()检查 ViewModelProvider 传入的 owner 是否为目标 Owner
内存泄漏Activity 销毁后 ViewModel 仍然存在ViewModel 内部持有 Activity 引用或长期后台协程严格使用 viewModelScope,避免引用 Activity
Hilt 注入失败构建时 @HiltViewModel 报错未在 Fragment 添加 @AndroidEntryPoint 或未引入 Hilt 依赖确保所有参与的 Fragment 均添加注解,并在模块 build.gradle 添加 Hilt 插件
跨模块依赖混乱编译时找不到 ViewModel 类SharedViewModel 模块未被依赖在需要使用的模块 build.gradle 中添加 implementation project(':shared')
调试技巧:在 Fragment 的 onCreate 或 onViewCreated 中打印 Log.d("ViewModelScope", "VM hash: ${viewModel.hashCode()}"),对比多个 Fragment 是否输出相同 hashCode。若不同,则作用域未对齐。

3.6 性能与稳定性优化

  • 减少共享数据体积:只在 SharedViewModel 中存放跨模块必要的聚合状态,避免存放列表大数据(如商品列表),可通过 Repository 按需加载。
  • 使用 StateFlow 替代 LiveData:配合 SharingStarted.WhileSubscribed(5000) 避免后台浪费。
  • 清理回调:SharedViewModel 中若注册了系统广播或监听器,务必在 onCleared() 中注销。
  • 避免跨模块直接暴露 ViewModel 对象:建议通过接口层(如 CartStateProvider)定义协议,模块只依赖接口。

3.7 模块化部署配置示例

// 顶层项目 settings.gradle.kts
include(":app", ":module:cart", ":module:product", ":module:common")

// 公共模块 build.gradle.kts(:module:common)
plugins {
    id("com.android.library")
    id("dagger.hilt.android.plugin")
    id("kotlin-kapt")
}
android { ... }
dependencies {
    implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")
}

// 产品模块 build.gradle.kts(:module:product)
plugins {
    id("com.android.library")
    id("dagger.hilt.android.plugin")
}
dependencies {
    implementation(project(":module:common"))
    implementation("androidx.hilt:hilt-navigation-fragment:1.0.0")
}
推荐工具:使用 hiltViewModel() 扩展函数自动根据 Fragment/Activity 的作用域创建 ViewModel,无需手动指定 Owner。