在以下代码中:

@Composable
fun Device(contentPadding: PaddingValues, modifier: Modifier = Modifier) {
    val vm:DeviceList = viewModel()
    vm.getDevices()
    var devices = vm.uiState.collectAsState();

    LazyColumn(contentPadding = contentPadding) {
        items(devices.value) { device -> DeviceItem(device) }
    }
}

vm.getDevices()个调用vm.uiState中所述的远程API和GET设备.

问题

正如代码清楚地显示的那样,它会导致无限的UI重组.vm.getDevices()会更新状态,而新状态为vm.uiState会导致用户界面重组.结果,vm.getDevices()被召回并再次更新状态.

我要找的是

我想要一个推荐的解决方案(最佳实践).此外,我可以放置一些肮脏的代码,例如IF/ELSE条件,以防止无限的UI重组.然而,我认为对于这种问题,可能会有一个更好的干净的解决方案.

编辑

class DeviceList : ViewModel() {

    private var deviceListUIState: MutableStateFlow<List<Device>> = MutableStateFlow(
        listOf()
    )

    val uiState
        get() = deviceListUIState.asStateFlow()

    fun getDevices() {
        viewModelScope.launch {
            try {
                val result: List<Device> = myApi.retrofitService.getDevices()
                deviceListUIState.value = result
            } catch (e: Exception) {
                Log.e(this.toString(), e.message ?: "")
            }
        }
    }
}

推荐答案

正如您已经发现的,您不应该请求任何数据作为合成的一部分--正如所解释的in the documentation,合成应该是无副作用的.除了这个无限的重组问题外,许多操作,如动画,都可能导致frequent recompositions.

要解决这个问题,你需要将所谓的getDevices从成分中移出.

有三种方法可以做到这一点:

Not the best: 1. Use an effect like 100

val vm:DeviceList = viewModel()
LaunchedEffect(vm) {
    vm.getDevices()
}
var devices = vm.uiState.collectAsState();

这会将调用从组合中移出,但仍需要在可组合代码中手动调用.这也意味着每次你回到这个屏幕(例如,屏幕‘进入合成’),它将被再次调用,而不是使用你已经加载的数据.

Better: 2. Load the data once when the ViewModel is created

class DeviceList : ViewModel() {

    private var deviceListUIState: MutableStateFlow<List<Device>> = MutableStateFlow(
        listOf()
    )

    val uiState
        get() = deviceListUIState.asStateFlow()

    init {
      // Call getDevices() only once when the ViewModel is created
      getDevices()
    }

    fun getDevices() {
        viewModelScope.launch {
            try {
                val result: List<Device> = myApi.retrofitService.getDevices()
                deviceListUIState.value = result
            } catch (e: Exception) {
                Log.e(this.toString(), e.message ?: "")
            }
        }
    }
}

通过在您的ViewModel的init中调用getDevices,它只被调用一次.这意味着逻辑根本不需要存在于您的Composable中:

// Just by calling this, the loading has already started
val vm:DeviceList = viewModel()
var devices = vm.uiState.collectAsState();

然而,这使得测试ViewModel变得相当困难,因为您无法准确控制加载开始的时间.

Best: 3. Make your ViewModel get its data from a cold Flow

与其使用单独的MutableStateFlow并使用viewModelScope.launch来填充它,不如使用Flow来封装数据的加载,然后只使用stateIn来存储流的结果:

class DeviceList : ViewModel() {

    val uiState = flowOf {
        val result: List<Device> = myApi.retrofitService.getDevices()
        // We got a valid result, send it to the UI
        emit(result)
    }.catch { e ->
        // Any exceptions the Flow throws, we can catch them here
        Log.e(this.toString(), e.message ?: "")
    }.stateIn(
        viewModelScope, // Save the result so the Flow only gets called once
        SharingStarted.Lazily,
        initialValue = listOf()
    )
}

我们仍然像上面看到的那样镇定自若:

val vm:DeviceList = viewModel()
val devices = vm.uiState.collectAsState();

但现在它是第一次在用户界面中调用collectAsState,这是flowOf的开始.这使得测试ViewModel变得很容易(因为您可以对它调用uiStatecollect来验证它是否返回您的值).

这也为将来使系统变得更智能提供了更大的灵活性-如果您后来添加了一个data layer和一个同时控制Retrofit数据和本地数据(例如,存储在数据库中的数据)的存储库,您可以很容易地用对存储库层的调用来替换flowOf {},交换源,而不会更改任何剩余的逻辑.

SharingStarted还允许你使用类似SharingStarted.WhileSubscribed(5000L)的东西--如果你实际上有Flow个数据一直在变化(比如,当用户在屏幕上时,你的推送消息改变了你的数据),这将确保在你的用户界面不可见(即你的应用程序在后台)时,你的ViewModel不会做不必要的工作,但一旦用户重新打开你的应用程序,它就会立即重启.

Android相关问答推荐

当我在Android上运行应用程序时,组件会随机调整大小和移动

无法在Android中创建通知频道

如何从sqlite数据库中检索数据到碎片android?

Modifer.Align()不适用于行/列/框中的文本.未解决的作用域实例错误

Jetpack Compose:带芯片的Textfield

更改活动(上下文)对接收到的uri的访问权限的影响?

Dispatchers中的Kotlin协同程序.Main没有';t块主螺纹

有没有办法迭代类型安全的项目访问器?

如何使用 Wea​​r OS 上的运行状况服务模拟位置?

使用 Gadle kotlin 为多模块 Android 代码库设置 jacoco

如何在 kotlin 中接收带有和不带有可空对象的集合并保持引用相同

当 Jetpack Compose 中的第一个文本很长时,将省略号添加到第一个文本

请求访问小部件中的位置权限

如何在 Jetpack Compose 中的特定位置绘制图像

Android 设备断开连接后发送的 BLE 蓝牙数据

Kotlin File(path).walkTopDown() 找不到文件

当包含可绘制对象的整数变量传递给 painterResource 函数时,为什么图像不在 Android Studio 中呈现?

组成不重叠的元素

JetPack Compose - 卡片行中的权重()不起作用

如何在android studio 2021.1中使用谷歌库以外的库