代码之家  ›  专栏  ›  技术社区  ›  A Farmanbar

如何在初始组合过程中在可组合函数中从远程API调用/获取数据[防止无限重组]

  •  0
  • A Farmanbar  · 技术社区  · 1 年前

    在以下代码中:

    @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() 调用远程API并获取中声明的设备 vm.uiState

    问题

    正如代码清楚地显示的那样,它会导致无限的UI重新组合。 vm.getDevices() 更新状态和新状态 vm.uiState 导致UI重新组合。因此 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 ?: "")
                }
            }
        }
    }
    
    2 回复  |  直到 1 年前
        1
  •  3
  •   ianhanniballake    1 年前

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

    要解决此问题,您需要移动称为 getDevices 构图不当。

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

    不是最好的:1。使用类似的效果 LaunchedEffect

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

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

    更好: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 ?: "")
                }
            }
        }
    }
    

    通过呼叫 getDevices 在中 init 在ViewModel中,它只被调用一次。这意味着逻辑根本不必存在于您的可组合文件中:

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

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

    最佳:3。使ViewModel从冷流中获取数据

    而不是单独 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 在UI中启动 flowOf 。这使得测试ViewModel变得容易(因为您可以调用 uiState collect 以验证它是否返回您的值)。

    这也为将来使系统更智能开辟了更多的灵活性-如果您稍后添加 data layer 以及一个控制改装数据和本地数据(例如,存储在数据库中的数据)的存储库,您可以轻松地替换 flowOf {} 通过对存储库层的调用,在不更改任何其他逻辑的情况下交换源代码。

    这个 SharingStarted 还允许您使用以下内容 SharingStarted.WhileSubscribed(5000L) -如果你真的有 对于一直在更改的数据(例如,当用户在屏幕上时,您收到了更改数据的推送消息),这将确保您的ViewModel在UI不可见时(即,您的应用程序在后台)不会做不必要的工作,但一旦用户重新打开应用程序,它会立即重新启动。

        2
  •  0
  •   Tristan Elliott    1 年前
    • 您的问题是:

    How to Call/Fetch Data from Remote API in a Composable Function

    是一种反模式,不应该这样做。您不应该直接从可组合文件内部发出网络请求。这将是一个 side effect 并且可组合函数应该是 side effect free