android compose ui 结合 ViewModel适配方案

如下以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 层只读取需要的基本类型索引,从而缩小重组范围。

相关推荐
2501_948120152 小时前
Android智能手机信息安全管理系统的研究
android·智能手机
DengDongQi2 小时前
Android Compose 应用中实现全局Dialog管理器的设计与实践
android
容华谢后2 小时前
Android消息推送 MQTT方案实践
android
_李小白3 小时前
【Android 美颜相机】第十五天:GPUImage3x3TextureSamplingFilter 解析
android·数码相机
Whisper_Sy3 小时前
Flutter for OpenHarmony移动数据使用监管助手App实战 - 月报告实现
android·开发语言·javascript·网络·flutter·ecmascript
天才少年曾牛3 小时前
Android 怎么写一个AIDL接口?
android
冬奇Lab3 小时前
【Kotlin系列14】编译器插件与注解处理器开发:在编译期操控Kotlin
android·开发语言·kotlin·状态模式
橘子134 小时前
MySQL表的约束(五)
android·mysql·adb
2501_915918414 小时前
Wireshark、Fiddler、Charles抓包工具详细使用指南
android·ios·小程序·https·uni-app·iphone·webview