如下以ExposedDropdownMenuBox下拉框控件为例子,描述android compose ui 结合 ViewModel适配方案。
一、Composable ui 组件 绑定状态变量的方法
1.方法1 by remember(selectedIndex)
方法1 by remember(selectedIndex) 参数列表定义了依赖项,只有当这些依赖项发生变化时,remember 内部的计算才会重新执行。
这意味着当 selectedIndex 改变时,mutableStateOf(options[selectedIndex]) 会自动重新计算,生成新的状态值。
Kotlin
/**
* 下拉列表组件1 回调选中索引 by remember(selectedIndex)
* @param name 下拉框名称(用于日志)
* @param selectedIndex 选中的索引
* @param onIndexChange 索引变化的回调(外部实现,如更新ViewModel的索引)
* @param options 选项列表
* @param modifier 修饰符
* @param width 下拉框宽度
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DropdownMenuBoxTest1(
name: String,
selectedIndex: Int,
onIndexChange: (Int) -> Unit,
options: List<String>,
modifier: Modifier = Modifier,
width: Dp = 120.dp
) {
Logger.i(TAG, "DropdownMenuBoxTest1 called - selectedIndex=$selectedIndex")
var expanded by remember { mutableStateOf(false) }
//方法1 by remember(selectedIndex) 参数列表定义了依赖项,只有当这些依赖项发生变化时,remember 内部的计算才会重新执行。
//这意味着当 selectedIndex 改变时,mutableStateOf(options[selectedIndex]) 会自动重新计算,生成新的状态值。
var selectedOption by remember(selectedIndex) {
mutableStateOf(options[selectedIndex])
}
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
Logger.i(TAG, "$name onExpandedChange expanded:$expanded,it:$it")
//这里 it值一直是false,只要expanded取反就行了
expanded = !expanded
},
modifier = modifier
) {
TextField(
value = selectedOption,
onValueChange = {
selectedOption = it
Logger.i(
TAG,
"疲劳等级 onValueChange selectedOption:$selectedOption"
)
},
readOnly = true,
// label = { Text("疲劳等级", fontSize = 14.sp) },
modifier = Modifier
.width(width)
.menuAnchor()
.padding(vertical = 0.dp), // 调整上下边距,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEachIndexed { index, option ->
DropdownMenuItem(
text = { Text(option, fontSize = 14.sp) },
onClick = {
//点击下拉框后,更新控件关联的状态值触发重组
onIndexChange(index)
expanded = false
Logger.i(
TAG,
"DropdownMenuBoxTest1 $name ExposedDropdownMenu.onClick index:$index last selectedOption:$selectedOption"
)
}
)
}
}
}
}
2.方法2 by emember
方法2 by remember 使用 remember 时,Compose 会记住该状态的值,即使 Composable 重组,状态也不会丢失。
因此当 selectedIndex 变化时,selectedOption 不会自动更新。需要手动调用LaunchedEffect监听 selectedIndex 变化来更新 selectedOption。
从而才能保证selectedIndex 变化 控件可以自动重组。
Kotlin
/**
* 下拉列表组件1 回调选中索引 by remember
* @param name 下拉框名称(用于日志)
* @param selectedIndex 选中的索引
* @param onIndexChange 索引变化的回调(外部实现,如更新ViewModel的索引)
* @param options 选项列表
* @param modifier 修饰符
* @param width 下拉框宽度
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DropdownMenuBoxTest2(
name: String,
selectedIndex: Int,
onIndexChange: (Int) -> Unit,
options: List<String>,
modifier: Modifier = Modifier,
width: Dp = 120.dp
) {
Logger.i(TAG, "DropdownMenuBoxTest2 called - selectedIndex=$selectedIndex")
var expanded by remember { mutableStateOf(false) }
//方法2 by remember 使用 remember 时,Compose 会记住该状态的值,即使 Composable 重组,状态也不会丢失。
// 因此当 selectedIndex 变化时,selectedOption 不会自动更新。需要手动调用LaunchedEffect监听 selectedIndex 变化来更新 selectedOption。
var selectedOption by remember {
mutableStateOf(options[selectedIndex])
}
// 监听 selectedIndex 变化并更新 selectedOption
LaunchedEffect(selectedIndex) {
Logger.i(TAG, "$name LaunchedEffect selectedIndex change selectedIndex:$selectedIndex")
selectedIndex?.let { index ->
if (index >= 0 && index < options.size) {
selectedOption = options[index]
}
}
}
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
Logger.i(TAG, "$name onExpandedChange expanded:$expanded,it:$it")
//这里 it值一直是false,只要expanded取反就行了
expanded = !expanded
},
modifier = modifier
) {
TextField(
value = selectedOption,
onValueChange = {
selectedOption = it
Logger.i(
TAG,
"疲劳等级 onValueChange selectedOption:$selectedOption"
)
},
readOnly = true,
// label = { Text("疲劳等级", fontSize = 14.sp) },
modifier = Modifier
.width(width)
.menuAnchor()
.padding(vertical = 0.dp), // 调整上下边距,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
colors = ExposedDropdownMenuDefaults.textFieldColors()
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
options.forEachIndexed { index, option ->
DropdownMenuItem(
text = { Text(option, fontSize = 14.sp) },
onClick = {
//点击下拉框后,更新控件关联的状态值触发重组
onIndexChange(index)
expanded = false
Logger.i(
TAG,
"DropdownMenuBoxTest2 $name ExposedDropdownMenu.onClick index:$index last selectedOption:$selectedOption"
)
}
)
}
}
}
}
总结:
by:每次重组都会执行
by emember:再次重组不会执行,在重组时始终返回之前存储的值,不重新计算。
by remember(selectedIndex):只有当依赖项 selectedIndex 改变时才重新计算会执行。
推荐使用1.方法1
二、ViewModel中创建可观察状态对象及使用方法。
ViewModel类定义如下:
Kotlin
class TestViewModel(application: Application) : AndroidViewModel(application) {
//方案1
/**
* 疲劳等级 下拉框选中索引.
*/
val selectedFatigueIndex = mutableIntStateOf(0)
//region 方案2 mutableStateOf Bean类型数据
// 使用 mutableStateOf 替代 MutableLiveData
private val _simulatedSceneBean = mutableStateOf(SimulatedSceneBean())
// 提供给外部观察的只读状态
val simulatedSceneBean: State<SimulatedSceneBean> = _simulatedSceneBean
// 更新数据的方法
fun updateSceneBean(bean: SimulatedSceneBean) {
_simulatedSceneBean.value = bean
}
//region 方案3 MutableLiveData 类型数据
// 创建 MutableLiveData 对象变量
private val _simulatedSceneBeanLiveData = MutableLiveData<SimulatedSceneBean>()
// 提供给外部观察的 LiveData 只读变量
val simulatedSceneBeanLiveData: MutableLiveData<SimulatedSceneBean> = _simulatedSceneBeanLiveData
// 初始化默认值
init {
_simulatedSceneBeanLiveData.value = SimulatedSceneBean()
}
// 更新LiveData类型数据的方法
fun updateLiveDataSceneBean(bean: SimulatedSceneBean) {
_simulatedSceneBeanLiveData.value = bean
}
//endregion
//endregion
//region 下拉框选项数据常量
// 疲劳等级选项
val fatigueLevels = application.resources.getStringArray(R.array.fatigueLevel).toList()
}
SimulatedSceneBean类定义如下:
Kotlin
data class SimulatedSceneBean(
/**
* 疲劳等级 下拉框选中索引.
*/
var selectedFatigueIndex: Int = 0,
/**
* 手势 下拉框选中索引.
*/
var selectedGestureIndex: Int = 0,
)
1.方案1绑定ViewModel中的mutableStateOf 基本类型
直接绑定ViewModel中的mutableStateOf可观察的状态对象。
当状态值发生变化时,它会通知 Compose 框架该状态已改变,从而触发相关 Composable 函数的重新组合,确保 UI 能够及时反映最新的状态值。
Kotlin
//方案1绑定ViewModel中的mutableStateOf 基本类型
DropdownMenuBoxTest1(
name = "疲劳等级Test1 selectedFatigueIndex",
selectedIndex = selectedFatigueIndex,
onIndexChange = {
viewModel.selectedFatigueIndex.intValue = it //使用selectedFatigueIndex参数
},
options = viewModel.fatigueLevels,//fatigueLevels,
modifier = Modifier.align(Alignment.CenterVertically),
width = 200.dp
)
2.方案2绑定ViewModel中的mutableStateOf Bean类型。
因为调用 DropdownMenuBoxTest1和DropdownMenuBoxTest2 控件都引用了viewModel.simulatedSceneBean,
所以每次simulatedSceneBean的任何变量更新,都会触发所有引用SimulatedSceneBean控件重组。
实测点击 DropdownMenuBoxTest1下拉列表更新SimulatedSceneBean,日志打印会同时触发 DropdownMenuBoxTest1和DropdownMenuBoxTest2重组。
Kotlin
//方案2绑定ViewModel中的mutableStateOf Bean类型
Row {
val sceneBean by viewModel.simulatedSceneBean
DropdownMenuBoxTest1(
name = "疲劳等级Test1 sceneBean!!.selectedFatigueIndex",
selectedIndex = sceneBean!!.selectedFatigueIndex,
onIndexChange = {
viewModel.updateSceneBean(sceneBean!!.copy(selectedFatigueIndex = it))
},
options = viewModel.fatigueLevels,//fatigueLevels,
modifier = Modifier.align(Alignment.CenterVertically),
width = 200.dp
)
}
Row {
val sceneBean2 by viewModel.simulatedSceneBean
DropdownMenuBoxTest2(
name = "疲劳等级Test1 sceneBean2!!.selectedGestureIndex",
selectedIndex = sceneBean2!!.selectedGestureIndex,
onIndexChange = {
viewModel.updateSceneBean(sceneBean2!!.copy(selectedGestureIndex = it))
},
options = viewModel.gestureOptions,//fatigueLevels,
modifier = Modifier.align(Alignment.CenterVertically),
width = 200.dp
)
}
3.方案3绑定ViewModel中的MutableLiveData Bean类型
MutableLiveData 的状态更新是通过 setValue 或 postValue 来触发,然后通过观察者模式通知 UI 更新。在 Compose 中,observeAsState() 会监听 LiveData 的变化并触发重组。
重组关系同方案2
Kotlin
//方案3绑定ViewModel中的MutableLiveData Bean类型
val sceneBeanLiveData by viewModel.simulatedSceneBeanLiveData.observeAsState()
DropdownMenuBoxTest1(
name = "疲劳等级Test1 sceneBeanLiveData!!.selectedFatigueIndex",
selectedIndex = sceneBeanLiveData!!.selectedFatigueIndex,
onIndexChange = {
viewModel.updateLiveDataSceneBean(sceneBeanLiveData!!.copy(selectedFatigueIndex = it))
},
options = viewModel.fatigueLevels,//fatigueLevels,
modifier = Modifier.align(Alignment.CenterVertically),
width = 200.dp
)
总结:推荐使用1.方案1
只要 Composable 读取了 viewModel.simulatedSceneBean(即订阅了该 State),任何对该 State 的更新都会让所有订阅者重组。
最佳做法:在 ViewModel 中把每个可变字段单独暴露为 State,或在 UI 层只读取需要的基本类型索引,从而缩小重组范围。