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

相关推荐
工程师老罗7 小时前
如何在Android工程中配置NDK版本
android
Libraeking10 小时前
破壁行动:在旧项目中丝滑嵌入 Compose(混合开发实战)
android·经验分享·android jetpack
市场部需要一个软件开发岗位10 小时前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
御承扬11 小时前
鸿蒙NDK UI之文本自定义样式
ui·华为·harmonyos·鸿蒙ndk ui
JMchen12312 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
crmscs13 小时前
剪映永久解锁版/电脑版永久会员VIP/安卓SVIP手机永久版下载
android·智能手机·电脑
localbob13 小时前
杀戮尖塔 v6 MOD整合版(Slay the Spire)安卓+PC端免安装中文版分享 卡牌肉鸽神作!杀戮尖塔中文版,电脑和手机都能玩!杀戮尖塔.exe 杀戮尖塔.apk
android·杀戮尖塔apk·杀戮尖塔exe·游戏分享
一起养小猫13 小时前
Flutter for OpenHarmony 实战_魔方应用UI设计与交互优化
flutter·ui·交互·harmonyos
机建狂魔13 小时前
手机秒变电影机:Blackmagic Camera + LUT滤镜包的专业级视频解决方案
android·拍照·摄影·lut滤镜·拍摄·摄像·录像
hudawei99613 小时前
flutter和Android动画的对比
android·flutter·动画