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

问题描述 投票:0回答:1

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

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

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

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

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

示例代码

//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 android-jetpack-compose android-viewmodel android-jetpack-navigation
1个回答
0
投票

handleResponse
中的重组和多次调用
SecondPageScreen
的问题是由于
viewModel.getApiStatus()
状态流被收集在
HandleApiResponse
可组合函数中而导致的,每次组合发生变化时都会重新组合。结果,状态流被收集了很多次,并且对
handleResponse
进行了多次调用。要解决此问题,请将代码更改为仅在
apiStatus
可组合项之外收集一次
HandleApiResponse
状态流,然后将收集到的值作为参数发送到
HandleApiResponse
。因此,状态流只会在父可组合项中收集一次,而不是在子可组合项中频繁收集。

@Composable
fun SecondPageScreen(
    viewModel: SampleViewModel,
    navHostController: NavHostController
) {
    val apiStatus = viewModel.getApiStatus().collectAsStateWithLifecycle()
    
    Box(modifier = Modifier.fillMaxSize()) {
        HandleApiResponse(
            modifier = Modifier.padding(top = 96.dp).align(Alignment.Center),
            viewModel = viewModel,
            navHostController = navHostController,
            apiStatus = apiStatus.value
        )

        Column(
            modifier = Modifier.padding(24.dp).align(Alignment.TopCenter),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            // ... Rest of your code
        }
    }
}

@Composable
fun HandleApiResponse(
    modifier: Modifier,
    viewModel: SampleViewModel,
    navHostController: NavHostController,
    apiStatus: FakeApiState
) {
    Log.d("Collect Second Page", "Collect Handle Api response composable function")
    val context = LocalContext.current

    when (apiStatus) {
        FakeApiState.Loading -> {
            // ... Loading UI
        }

        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 -> {
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.