正如您已经发现的,您不应该请求任何数据作为合成的一部分--正如所解释的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变得很容易(因为您可以对它调用uiState
和collect
来验证它是否返回您的值).
这也为将来使系统变得更智能提供了更大的灵活性-如果您后来添加了一个data layer和一个同时控制Retrofit数据和本地数据(例如,存储在数据库中的数据)的存储库,您可以很容易地用对存储库层的调用来替换flowOf {}
,交换源,而不会更改任何剩余的逻辑.
SharingStarted
还允许你使用类似SharingStarted.WhileSubscribed(5000L)
的东西--如果你实际上有Flow
个数据一直在变化(比如,当用户在屏幕上时,你的推送消息改变了你的数据),这将确保在你的用户界面不可见(即你的应用程序在后台)时,你的ViewModel不会做不必要的工作,但一旦用户重新打开你的应用程序,它就会立即重启.