You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Kotlin Compose ViewModel共享列表更新异常及传参方案咨询

问题:Compose中跨组件共享ViewModel的实现方案合理性咨询

我在Android Kotlin Compose开发中,使用带TopBar的Scaffold,TopBar需要展示设备列表,因此创建了对应的ViewModel管理该列表。同时有一个可添加设备的Composable组件,希望添加设备后TopBar能同步显示更新后的列表。

最初尝试让两个组件共用同一ViewModel,但调用addDevice方法时列表每次都会重建,设备无法真正保存。之后改为在父Composable中创建ViewModel实例,再传递给相关子Composable,想咨询这个实现方案是否合理。


ViewModel代码

class ConnectableTopAppBarViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(ConnectableTopAppBarUiState())
    val uiState: StateFlow<ConnectableTopAppBarUiState> = _uiState.asStateFlow()

    fun addDevice() {
        _uiState.update { currentState ->
            val updatedDevices = ArrayList<Device>()
            for (device in currentState.devices) {
                device.isSelected = false
                updatedDevices.add(device)
            }
            updatedDevices.add(Device("Device ${updatedDevices.size}", "123.158.14.92", true))
            Log.d("TAG", "total: ${updatedDevices.size}")
            currentState.copy(
                selectedDeviceIndex = updatedDevices.size - 1,
                devices = updatedDevices
            )
        }
    }

    fun selectDevice(deviceIndex: Int) {
        _uiState.update { currentState ->
            var i = 0
            val updatedDevices = ArrayList<Device>()
            for (device in currentState.devices) {
                device.isSelected = i == deviceIndex
                updatedDevices.add(device)
                i += 1
            }
            currentState.copy(
                selectedDeviceIndex = deviceIndex,
                devices = updatedDevices
            )
        }
        Log.d("TAG", uiState.value.devices.joinToString(" "))
    }
}

UI状态类代码

data class ConnectableTopAppBarUiState(
    val selectedDeviceIndex: Int = 0,
    val devices: List<Device> = mutableListOf()
)

TopBar组件代码

@Composable
fun ConnectableTopAppBar(
    appState: CustomAppState,
    modifier: Modifier = Modifier,
    connectableTopAppBarViewModel: ConnectableTopAppBarViewModel = ConnectableTopAppBarViewModel()
) {
    val deviceUiState by connectableTopAppBarViewModel.uiState.collectAsState()
    Row(
        modifier = modifier
            .fillMaxWidth()
    ) {
        LazyRow(
            modifier = modifier.weight(1f),
            contentPadding = PaddingValues(horizontal = 16.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            // 展示设备
            itemsIndexed(deviceUiState.devices) { index, device ->
                    CustomButton(
                        onClick = { connectableTopAppBarViewModel.selectDevice(index) },
                        text = {
                            Text(
                                text = device.name,
                                style = MaterialTheme.typography.labelLarge
                            )
                        },
                        leadingIcon = {
                            Icon(
                                painter = painterResource(id = R.drawable.ic_wifi),
                                contentDescription = null
                            )
                        },
                    )
            }

            // 跳转至添加设备页面
            item {
                CustomOutlinedButton(
                    onClick = {
                        appState.navController.navigate(SubLevelDestination.CONNECT.route)
                    },
                    text = {
                        Text(
                            text = "Add Device",
                            style = MaterialTheme.typography.labelLarge
                        )
                    },
                    leadingIcon = {
                        Icon(imageVector = AppIcons.Add, contentDescription = null)
                    },
                )
            }
        }
    }
}

添加设备组件代码

@Composable
fun ConnectScreen(
    upPress: () -> Unit,
    modifier: Modifier = Modifier,
    connectableTopAppBarViewModel: ConnectableTopAppBarViewModel = ConnectableTopAppBarViewModel(),
) {
    Column(
        modifier = modifier
    ) {
        ChooseRemoteSectionTitle("Select Device To Add")
        LazyColumn(
            modifier = modifier,
            contentPadding = PaddingValues(horizontal = 16.dp),
            verticalArrangement = Arrangement.spacedBy(8.dp)
        ) {
            item {
                ConnectableDeviceItem(
                    addDevice = connectableTopAppBarViewModel::addDevice,
                    modifier = modifier,
                    upPress = upPress,
                    name = "Device Example 1"
                )
            }

            item {
                ConnectableDeviceItem(
                    addDevice = connectableTopAppBarViewModel::addDevice,
                    modifier = modifier,
                    upPress = upPress,
                    name = "Device Example 2"
                )
            }
        }
    }
}

@Composable
@OptIn(ExperimentalMaterial3Api::class)
private fun ConnectableDeviceItem(
    addDevice: () -> Unit,
    modifier: Modifier,
    upPress: () -> Unit,
    name: String
) {
    Card(
        modifier = modifier.fillMaxWidth(),
        onClick = {
            addDevice()
            upPress()
        }
    ) {
        Box(
            modifier = modifier
                .fillMaxWidth()
                .background(MaterialTheme.colorScheme.surface)
                .padding(5.dp),
            contentAlignment = Alignment.Center
        ) {
            Text(text = name)
        }
    }
}

修改后的传参代码

@Composable
fun RemoteControlApp() {
    val connectableTopAppBarViewModel = ConnectableTopAppBarViewModel()
    ....
ConnectableTopAppBar(
                    viewModel = connectableTopAppBarViewModel,
                    appState = appState
                )
....
remoteControlNavGraph(
                connectableTopAppBarViewModel = connectableTopAppBarViewModel,
                upPress = appState::upPress,
                paddingValues = paddingValues
            )
}

解答

方案合理性分析

你当前在父Composable中创建ViewModel实例并传递给子组件的方案是合理的,但存在更规范的优化方向,同时原代码中还有几个需要修正的问题:

  1. 原问题根因
    最初两个组件各自通过默认参数创建ViewModel实例,导致它们持有完全独立的ViewModel对象,操作的是不同的设备列表,所以添加设备后TopBar不会同步更新。

  2. 父组件传参方案的合理性
    这种方式能确保子组件共享同一个ViewModel实例,解决状态不同步的问题。但需要注意:

    • 父组件需保证自身重组时不会重新创建ViewModel实例。由于RemoteControlApp是根Composable,默认重组不会重新初始化局部变量,但更稳妥的方式是使用viewModel()函数(依赖androidx.lifecycle:lifecycle-viewmodel-compose库)获取ViewModel,它会自动管理实例生命周期,避免意外重建:
      @Composable
      fun RemoteControlApp() {
          val connectableTopAppBarViewModel = viewModel<ConnectableTopAppBarViewModel>()
          // ... 后续传递逻辑不变
      }
      
  3. 代码中的其他问题修正

    • UI状态类的可变列表问题:ConnectableTopAppBarUiState中devices默认值使用mutableListOf(),但data class属性应尽量用不可变类型,避免外部意外修改状态。建议改为:
      data class ConnectableTopAppBarUiState(
          val selectedDeviceIndex: Int = 0,
          val devices: List<Device> = emptyList()
      )
      
    • ViewModel中修改可变对象的问题:Device是可变类(直接修改device.isSelected),会导致状态更新不可预测。应将Device改为不可变数据类,修改状态时创建新实例:
      data class Device(val name: String, val ip: String, val isSelected: Boolean)
      
      然后在addDevice和selectDevice中通过copy()创建新Device实例:
      fun addDevice() {
          _uiState.update { currentState ->
              val updatedDevices = currentState.devices.map { it.copy(isSelected = false) } + 
                  Device("Device ${currentState.devices.size}", "123.158.14.92", true)
              currentState.copy(
                  selectedDeviceIndex = updatedDevices.size - 1,
                  devices = updatedDevices
              )
          }
      }
      
      fun selectDevice(deviceIndex: Int) {
          _uiState.update { currentState ->
              val updatedDevices = currentState.devices.mapIndexed { index, device ->
                  device.copy(isSelected = index == deviceIndex)
              }
              currentState.copy(
                  selectedDeviceIndex = deviceIndex,
                  devices = updatedDevices
              )
          }
      }
      
    • 组件默认参数优化:子组件的ViewModel参数应移除默认实例创建逻辑,强制父组件传递,避免误用:
      @Composable
      fun ConnectableTopAppBar(
          appState: CustomAppState,
          modifier: Modifier = Modifier,
          connectableTopAppBarViewModel: ConnectableTopAppBarViewModel // 移除默认值
      ) {
          // ... 内部逻辑不变
      }
      
      ConnectScreen同理。

更规范的共享ViewModel方式

除了父组件传参,还可以使用ViewModel的作用域绑定:

  • 如果两个组件属于同一个导航图,可以在导航图级别创建ViewModel,通过hiltViewModel()(使用Hilt)或viewModel()指定导航图作用域,让导航图内所有组件共享该ViewModel。
  • 对于全局共享状态,可使用单例ViewModel或Jetpack DataStore,但设备列表属于页面级状态,页面/导航图级别的ViewModel更合适。

内容的提问来源于stack exchange,提问作者StackerSapper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 06:10:02