Mr.*_*.AF 3 android kotlin android-jetpack-compose
在下面的代码中:
@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) }
}
}
Run Code Online (Sandbox Code Playgroud)
调用vm.getDevices()远程 API 并获取 中所述的设备vm.uiState。
正如代码清楚显示的那样,它会导致无限的 UI 重组。更新状态和导致 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 ?: "")
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
正如您所发现的,您不应该请求任何数据作为组合的一部分 - 正如文档中所解释的,组合应该没有副作用。除了这种无限重组问题之外,许多操作(例如动画)也会导致频繁的重组。
为了解决这个问题,你需要移动那些getDevices不合时宜的东西。
有三种方法可以做到这一点:
不是最好的: 1. 使用类似的效果LaunchedEffect
val vm:DeviceList = viewModel()
LaunchedEffect(vm) {
vm.getDevices()
}
var devices = vm.uiState.collectAsState();
Run Code Online (Sandbox Code Playgroud)
这会将调用移出组合,但仍需要在可组合代码中进行手动调用。这也意味着每次您返回到此屏幕(例如,屏幕“进入合成”)时,都会再次调用该方法,而不是使用您已经加载的数据。
更好:2.创建ViewModel时加载一次数据
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 ?: "")
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
通过调用getDevicesViewModelinit的 ,它只会被调用一次。这意味着逻辑根本不必存在于您的可组合项中:
// Just by calling this, the loading has already started
val vm:DeviceList = viewModel()
var devices = vm.uiState.collectAsState();
Run Code Online (Sandbox Code Playgroud)
但是,这使得测试 ViewModel 变得相当困难,因为您无法准确控制加载开始的时间。
最好:3.让你的 ViewModel 从冷流中获取数据
不要单独使用 aMutableStateFlow来viewModelScope.launch填充它,而是使用 aFlow来封装数据的加载,然后使用以下方法存储该 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()
)
}
Run Code Online (Sandbox Code Playgroud)
我们仍然得到与上面看到的相同的可组合项:
val vm:DeviceList = viewModel()
val devices = vm.uiState.collectAsState();
Run Code Online (Sandbox Code Playgroud)
但现在它是 UI 中的第一个调用,collectAsState从而启动了flowOf. 这使得测试 ViewModel 变得容易(因为您可以调用uiState和collect来验证它是否返回您的值)。
这也为将来使系统变得更加智能提供了更大的灵活性 - 如果您稍后添加一个数据层和一个控制 Retrofit 数据和本地数据(例如,存储在数据库中的数据)的存储库,您可以轻松地将其替换flowOf {}为调用您的存储库层,交换源代码而不更改任何其余逻辑。
它还SharingStarted允许您使用类似的东西SharingStarted.WhileSubscribed(5000L)- 如果您实际上有一个Flow一直在变化的数据(例如,当用户在该屏幕上时您有更改数据的推送消息),这将确保您的 ViewModel 不会当您的 UI 不可见时(即您的应用程序在后台)执行不必要的工作,但一旦用户重新打开您的应用程序,就会立即重新启动。