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

相关推荐
DogDaoDao2 小时前
Android 硬件编码器参数完全指南:MediaCodec 深度解析
android·音视频·视频编解码·h264·硬编码·视频直播·mediacodec
JohnnyDeng943 小时前
Android 自定义 View:Canvas 绘图与事件分发深度解析
android
薛定猫AI6 小时前
【深度解析】Gemini Omni 多模态生成与 Agent 化创作工作流:从视频编辑到 UI 生成的技术演进
人工智能·ui·音视频
Android小码家7 小时前
Framework之Launcher小窗开发
android·framework·虚拟屏·小窗
赏金术士7 小时前
第七章:状态管理实战与架构总结
android·ui·kotlin·compose
颂love8 小时前
MySQL的执行流程
android·数据库·mysql
幽络源小助理11 小时前
全新UI 阅后即焚V2正式版系统源码_全开源_安全加密传输
安全·ui·开源·php源码
云起SAAS12 小时前
抖音小游戏源码 - 消消乐 | 含激励广告+成就系统 | 开箱即用商业级消除游戏模板
android·游戏·广告联盟·看激励广告联盟流量主·抖音小游戏源码 - 消消乐
大貔貅喝啤酒14 小时前
基于Windows下载安装Android Studio 3.3.2版本教程(2026详细图文版)
android·java·windows·android studio
程序员码歌14 小时前
OpenSpec 到 Superpowers:AI 编码从说清到做对
android·前端·人工智能