KotlinCompose Android -视图模型中可组合对象之间的共享列表

7eumitmz  于 2023-03-30  发布在  Kotlin
关注(0)|答案(1)|浏览(151)

我有一个带顶杆的脚手架。
topBar应该显示一个项目列表(设备列表)。因此我为它创建了一个视图模型。
我也有一个组合,让您添加一个项目到该列表(添加一个新的设备到列表)。我希望topBar显示更新的列表。
我使用相同的视图模型获取列表并向列表中添加一个项。

问题是调用了addDevice方法,但每次都好像在重新创建列表,所以Device并没有真正保存在列表中。

这是视图模型:

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(" "))
    }
}

ConnectableTopAppBarUiState

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

这是显示设备并导航到屏幕的顶部栏,我们可以在其中添加设备:

@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)
        ) {
            
            // Here I display the devices
            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
                            )
                        },
                    )
            }

            // Here I navigate to the screen where we can add more devices
            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)
                    },
                )
            }
        }
    }
}

这是我添加Device的compose:

@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
            )
}

这是一个好方法吗?

5cnsuln7

5cnsuln71#

我看到了主要的问题...你永远不能自己调用ViewModel类的构造函数,除非在它的工厂中,如果它有一个。(在这种情况下,不需要工厂,因为你的ViewModel类有一个空的构造函数。)
由于您直接调用ViewModel的构造函数,因此您有ViewModel的多个示例。一个示例中的更改不会影响其他示例。
用调用viewModel()来替换对ConnectableTopAppBarViewModel()的调用。这是获取对ViewModel的引用的正确方法,如果它们都使用相同的函数,则可以获取在组合中其他地方使用的相同示例。
我看到了另一个潜在的问题...你的Device类是可变的,但你试图将它用作你的State的一部分。这是非常容易出错的。我将给予一个你可以创建的问题的例子。为了简化,请想象selectedDeviceIndex属性不存在,您跟踪选择了哪个项目的唯一方法是使用列表中每个设备的isSelected属性。设置所选设备的功能可以简化为:

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)
        }
        currentState.copy(
            devices = updatedDevices
        )
    }
}

请注意,您在新列表中重用了与旧列表中相同的Device示例。您只是改变了它们,而没有复制它们。
现在,当MutableState对象比较新旧Lists时,它不会检测到任何更改,因为旧列表的内容指向相同的Device示例,因此旧列表中已经有了新状态。因此,当您更改所选设备时,不会发生重组!
但是,您还将所选设备存储在单独的属性selectedDeviceIndex中,因此这可能在一定程度上避免了上述问题。重新组合将发生,但Compose中的其他可能优化可能无法正常工作。
另一个问题是你有两个真实的来源。你可以通过查看selectedDeviceIndex或检查每个设备上的isSelected来找到哪个项目被选中。这导致你每次想要改变状态的这个方面时都必须改变两个真实的来源,这是非常容易出错的。这只会给你的代码带来更多的bug机会,两个来源可能不一致。
我的建议是从Device类中删除isSelected属性,并添加一个扩展函数:

fun Device.isSelected(owningState: ConnectableTopAppBarUiState) =
    owningState.devices[owningState.selectedDeviceIndex] === this

然后,ViewModel函数可以简化为:

fun addDevice() {
    _uiState.update { currentState ->
        val newDeviceIndex = currentState.devices.size
        val newDevice = Device("Device $newDeviceIndex", "123.158.14.92", true))
        Log.d("TAG", "total: ${updatedDevices.size}")
        currentState.copy(
            selectedDeviceIndex = newDeviceIndex,
            devices = currentState.devices + newDevice 
        )
    }
}

fun selectDevice(deviceIndex: Int) {
    _uiState.update { currentState ->
        currentState.copy(
            selectedDeviceIndex = deviceIndex,
        )
    }
    Log.d("TAG", uiState.value.devices.joinToString(" "))
}

相关问题