在可组合函数中使用 collectAsStateWithLifecycle() 方法时会触发多个重构

Multiple recompositions are triggered while using collectAsStateWithLifecycle() method in a composable function

提问人:Vikram Ragu 提问时间:10/30/2023 最后编辑:Vikram Ragu 更新时间:10/31/2023 访问量:92

问:

我有一个带有 NavHost 的 Component 活动,它有 2 个可组合项,代表 2 个屏幕。在第一个屏幕可组合项中,我收集了一个计数器值,该计数器值是 viewModel 中存在的状态流,用于保存计数值。在第二个屏幕中,我根据 API 调用成功递增视图模型中的计数器值。API调用是从单击按钮触发的,成功后,我递增计数器并弹出返回第一个屏幕的后退堆栈以显示递增的计数器值。ViewModel 在两个可组合屏幕之间共享。

在可组合函数中收集视图模型中存在的状态流的两种方法是

  1. viewModel.apiStatus.collectAsState()
  2. viewModel.apiStatus.collectAsStateWithLifecycle()

我知道后者是推荐的方式,因为它的生命周期感知。 但是当我使用 collectAsStateWithLifecycle() 收集 apiStatus 时 并尝试弹出 backstack 以转到第一个屏幕,第二个屏幕一遍又一遍地重组并多次收集值并多次递增计数器,即多次调用 handleResponse() 方法。

任何有助于确定为什么会发生这种副作用以及如何解决这个问题的帮助都将对我对 compose 的理解非常有帮助。

示例代码

//called from setContent{} in MainActivity

@Composable
fun SampleNavSetup(sampleViewModel: SampleViewModel) {

    val navController = rememberNavController()

    NavHost(navController = navController,
        startDestination = SampleNav.FirstPage.route) {

       this.composable(route = SampleNav.FirstPage.route) {
           FirstPageScreen(
               viewModel = sampleViewModel,
               navHostController = navController
           )
        }

        this.composable(route = SampleNav.SecondPage.route){
           SecondPageScreen(
               viewModel = sampleViewModel,
               navHostController = navController
           )
        }
    }
 }

 @Composable
 fun FirstPageScreen(
     viewModel: SampleViewModel,
     modifier: Modifier = Modifier,
     navHostController: NavHostController
  ) {
   Column(
       modifier = modifier.fillMaxSize().padding(24.dp),
       horizontalAlignment = Alignment.CenterHorizontally
   ) {

      val result = viewModel.getIncrementCounter().collectAsStateWithLifecycle()

      Text(
        fontSize = 18.sp,
        fontWeight = FontWeight.W600,
        text = "Counter value is = ${result.value}"
      )

      Button(
         modifier = Modifier.padding(top = 36.dp),
         onClick = {
            navHostController.navigate(SampleNav.SecondPage.route)
          }
      ) {
        Text(
            text = "Nav to next",
            color = Color.White
        )
      }
    }
 }

@Composable
fun SecondPageScreen(
   viewModel: SampleViewModel,
   navHostController: NavHostController) {

   Box(modifier = Modifier.fillMaxSize()) {

       HandleApiResponse(
           modifier = Modifier.padding(top = 96.dp).align(Alignment.Center),
           viewModel = viewModel,
           navHostController = navHostController
       )

       Column(modifier = Modifier.padding(24.dp).align(Alignment.TopCenter),
           horizontalAlignment = Alignment.CenterHorizontally
       ) {

           Text(
               fontSize = 18.sp,
               fontWeight = FontWeight.W600,
               text = "Make Fake API to increment counter"
           )

           Button(
               modifier = Modifier.padding(top = 36.dp),
               onClick = {
                  viewModel.doFakeApiCall()
               }
           ) {
               Text(
                  text = "Click Me",
                  color = Color.White
               )
           }
        }
     }
  }


 @Composable
 fun HandleApiResponse(
     modifier: Modifier,
     viewModel: SampleViewModel,
     navHostController: NavHostController
  ) {

    Log.d("Collect Second Page","Collect Handle Api response composable function")
    val context = LocalContext.current

    //does not trigger multiple times with collectAsState()
    //val response = viewModel.getApiStatus().collectAsState()

    /*But with collectAsStateWithLifeCycle() it is observed multiple
     times as the screen is recomposed multiple times & handleResponse is called 
     multiple times, don't know the reason why*/
 
    val response = viewModel.getApiStatus().collectAsStateWithLifecycle()

    when (response.value) {

    FakeApiState.Loading -> {
        CircularProgressIndicator(modifier = modifier.size(32.dp),
        color = Color.Green)
    }

    FakeApiState.Success -> {
        Log.d("Collect Second Page","Collect Api status")
        handleResponse(viewModel = viewModel,navHostController=navHostController)
     }

      FakeApiState.Fail -> {
        Toast.makeText(context, "Api Fail", Toast.LENGTH_SHORT).show()
      }

      FakeApiState.Initial -> {}
   }
}

private fun handleResponse(
    viewModel: SampleViewModel,
    navHostController: NavHostController
) {
    Log.d("Second Page Response", "Collect Handle Response & popBackStack()")
    viewModel.incrementCounter()
    navHostController.popBackStack()
    viewModel.clearApiStatus()
 }


class SampleViewModel : ViewModel() {

    private val _incrementCounter = MutableStateFlow(0)
    fun getIncrementCounter() = _incrementCounter.asStateFlow()

    private val _apiStatus = MutableStateFlow(FakeApiState.Initial)
    fun getApiStatus() = _apiStatus.asStateFlow()


    fun incrementCounter() {
       _incrementCounter.value = _incrementCounter.value + 1
    }

    fun clearApiStatus() {
       _apiStatus.value = FakeApiState.Initial
    }

    fun doFakeApiCall() {
       _apiStatus.value = FakeApiState.Loading
        viewModelScope.launch {
        delay(1500L)
        _apiStatus.value = FakeApiState.Success
     }
   }
}
安卓 android-jetpack-compose android-viewmodel android-jetpack-navigation

评论


答: 暂无答案