Android Jetpack Compose+Hilt下ViewModel间传值的最佳实践
我正在使用Jetpack Compose和Hilt构建Android应用,遵循MVVM模式。现有两个Composable函数:
@Composable fun TimerView( viewModel: TimerViewModel = hiltViewModel(), ) {...}
和
@Composable fun SensorScreen(viewModel: SensorViewModel = hiltViewModel()) { val values = viewModel.recentValues.mapValues { it.value.collectAsState(listOf(0f)) } Column( modifier = Modifier.verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { TimerView() // 其他代码 } }
其中TimerView嵌套在SensorScreen中,二者各有独立的ViewModel。TimerViewModel代码如下:
@HiltViewModel class TimerViewModel @Inject constructor( ) : ViewModel() { val totalTime = 100 private val _time = MutableStateFlow(totalTime) val time = _time.asStateFlow() init { viewModelScope.launch { while (true) { delay(1000) _time.value = Integer.max(0, _time.value - 1) if (_time.value == 0) { onTimeFinished() break } } } } // 假设存在的计时结束处理方法 private fun onTimeFinished() { // 处理逻辑 } }
我希望从父ViewModel(SensorViewModel)传递totalTime值给TimerViewModel,请问如何做才符合最佳实践和MVVM模式?
要实现从SensorViewModel向TimerViewModel传递动态totalTime,同时遵循MVVM和Hilt的最佳实践,**Hilt辅助注入(Assisted Injection)**是官方推荐的方案,它能妥善处理ViewModel的动态构造参数,同时保证生命周期安全。
步骤1:添加必要依赖
确保项目中引入Hilt辅助注入相关依赖:
implementation "androidx.hilt:hilt-navigation-compose:1.1.0" implementation "com.google.dagger:hilt-android:2.48" kapt "com.google.dagger:hilt-android-compiler:2.48" implementation "androidx.hilt:hilt-assisted:1.1.0" kapt "androidx.hilt:hilt-compiler:1.1.0"
步骤2:改造TimerViewModel
使用@AssistedInject和@Assisted注解声明动态参数,并创建对应的辅助工厂和ViewModel Provider:
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.CreationExtras import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch class TimerViewModel @AssistedInject constructor( @Assisted private val totalTime: Int // 动态传入的总时长 ) : ViewModel() { private val _time = MutableStateFlow(totalTime) val time = _time.asStateFlow() init { startTimer() } private fun startTimer() { viewModelScope.launch { while (true) { delay(1000) _time.value = Integer.max(0, _time.value - 1) if (_time.value == 0) { onTimeFinished() break } } } } private fun onTimeFinished() { // 计时结束的处理逻辑 } // 定义辅助工厂,用于创建ViewModel实例 @dagger.assisted.AssistedFactory interface Factory { fun create(totalTime: Int): TimerViewModel } // 提供ViewModelProvider.Factory,供Compose调用 companion object { fun provideFactory( factory: Factory, totalTime: Int ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { return factory.create(totalTime) as T } } } }
步骤3:在Composable中传递参数并创建ViewModel
修改TimerView使其接收totalTime参数,通过辅助工厂创建ViewModel;同时在SensorScreen中传递来自SensorViewModel的totalTime值:
@Composable fun TimerView( totalTime: Int, viewModel: TimerViewModel = hiltViewModel( factory = TimerViewModel.provideFactory( hiltViewModel<TimerViewModel.Factory>(), totalTime = totalTime ) ) ) { // TimerView的UI逻辑,例如展示viewModel.time的状态 } @Composable fun SensorScreen(viewModel: SensorViewModel = hiltViewModel()) { val values = viewModel.recentValues.mapValues { it.value.collectAsState(listOf(0f)) } // 从SensorViewModel获取需要传递的totalTime val timerTotalTime = viewModel.timerTotalTime // 替换为你实际的属性名 Column( modifier = Modifier.verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { TimerView(totalTime = timerTotalTime) // 其他UI代码 } }
方案优势
- 符合MVVM原则:ViewModel的所有依赖(包括动态参数)在创建时注入,保证了ViewModel的独立性和可测试性——单元测试中可轻松传入不同的
totalTime值进行验证。 - 遵循Hilt设计规范:辅助注入是Hilt官方针对动态ViewModel参数的解决方案,避免了手动管理ViewModel生命周期的风险。
- Composable简洁性:通过
hiltViewModel函数绑定生命周期,无需手动处理配置变更等场景下的ViewModel重建问题。
备选方案(仅适用于简单场景)
如果场景非常简单,也可以在TimerViewModel中添加初始化方法,在Composable中传递参数后调用,但这种方案不符合ViewModel状态初始化的最佳实践,可能导致状态不一致:
// TimerViewModel添加方法 fun initTimer(newTotalTime: Int) { _time.value = newTotalTime // 若之前计时已启动,需先取消再重启 } // TimerView实现 @Composable fun TimerView( totalTime: Int, viewModel: TimerViewModel = hiltViewModel(), ) { LaunchedEffect(totalTime) { viewModel.initTimer(totalTime) } // UI逻辑 }
该方案的问题在于ViewModel状态依赖外部触发初始化,违反了MVVM中ViewModel自我管理状态的原则,配置变更时可能出现状态错乱,因此仅推荐在简单场景下临时使用。
内容的提问来源于stack exchange,提问作者tommy

