副作用问题 - Jetpack Compose 中的 LaunchedEffect 和 SideEffect

发布于 2025-01-11 23:33:21 字数 3691 浏览 0 评论 0原文

为什么每次我的可组合项失效时都会调用 SideEffect,但 LaunchedEffect 却不然?

sealed class SomeState {
 object Error:SomeState()
 data class Content(): SomeState
}

class MyViewModel:ViewModel {
  internal val response: MutableLiveData<SomeState> by lazy {
    MutableLiveData<SomeState>()
  }
}

// This is top-level composable, it wont be recomposed ever
@Composable
fun MyComposableScreen(
viewModel:MyVm,
launchActivity:()->Unit
){
  val someDialog = remember { mutableStateOf(false) }

  MyComposableContent()

  GenericErrorDialog(someDialog = someDialog)

  when (val state = viewModel.response.observeAsState().value) {
    // Query 1  
    is Content -> LaunchedEffect(Unit) { launchActivity() }
    Error -> {
      // Query 2
  // Gets called everytime this composable gets invalidated, for eg in case of TextField change, compiler is invalidating it.
 // But if i change it to LaunchedEffect(Unit), invalidation has no effect,LaunchedEffect only gets called when there is new update to the LiveData. why?
      SideEffect { someDialog.value = true}
    }
  }
}

// This is the content, which can be recomposed in case of email is changed
@Composable
fun MyComposableContent(
onEmailChange:(email) -> Unit,
email:String,
){
  TextField(
   email = email,
   onValueChange = onEmailChange
  )
}

我对查询 1 和查询 2 都有疑问,它们都是顶级可组合项的一部分,永远不会重新组合,但可以无效,

when (val state = viewModel.response.observeAsState().value) { // observing to live-data
        // Query 1  
        is Content -> LaunchedEffect(Unit) { launchActivity() }
        Error -> {
          // Query 2
          SideEffect { someDialog.value = true}
        }
      }

如果是,

Content -> LaunchedEffect(Unit) { launchActivity() }

我相信这应该没问题,因为我们只想在以下情况下启动活动LaunchedEffect 是第一次组合的一部分,如果实时数据状态是内容,它只是组合的一部分

我在第二个场景中遇到问题,

Error -> {
   // Query 2
  SideEffect { someDialog.value = true // shows a dialog} 
}

如果 live-data 的最后状态代码>,是错误在视图模型中。每次我在 TextField 中进行更改时,我的顶级 MyComposableScreen 都会被撰写编译器无效(未重新组合),并且自上次状态以来live-data 被设置为错误,SideEffect 每次都在运行,这很好,因为它应该在每次成功的合成和重新合成时运行。

但是,如果我将其从 SideEffect 更改为 LaunchedEffect(Unit){someDialog.value = true} 对话框不会每次都显示 MyComposableScreen无效,这就是期望的行为。

LaunchedEffect(Unit) gets called only if there live-data emits the new state again because of any UI-action.

但是,我不确定其背后的原因,为什么 LaunchedEffect(Unit){someDialog.value = true} 中的代码在可组合项 invalidated 后不会触发,但可组合项失效后,SideEffect 内的代码会被触发吗?


为了更清楚地说明

我理解的区别

SideEffect ->每一次成功的构图和重新构图(如果它是其中的一部分) LaunchedEffect ->当它进入合成并跨过重新合成时,除非键被更改。

但在上面的场景中 - 特别是这段代码

@Composable
fun MyTopLevelComposable(viewModel:MyViewModel){

  when (val state = viewModel.response.observeAsState().value) { // observing live-data state
    is Content -> LaunchedEffect(Unit) { launchActivity() }
    Error -> SideEffect { someDialog.value = true}
  }
}

它永远不会被重组。再次调用此可组合项的唯一原因可能是组合编译器使视图无效。

我的查询是 ->当 view/composable 失效时

SideEffect {someDialog.value = true} 执行,因为它将再次进行组合而不是重新组合,因为 viewModel.response(这是实时数据)最后的状态是 Error

但如果将其更改为 LaunchedEffect(Unit) {someDialog.value = true} 它在可组合项无效后不会再次执行。它只对实时数据发出的新状态做出反应。

问题是为什么?无效应该重新开始合成,因为它是合成。在这种情况下,LaunchedEffect 的行为应与 SideEffect 类似,因为两者都对 composition 做出反应。

Why SideEffect gets called ever-time my composable is invalidated , but same does-not hold true for LaunchedEffect?

sealed class SomeState {
 object Error:SomeState()
 data class Content(): SomeState
}

class MyViewModel:ViewModel {
  internal val response: MutableLiveData<SomeState> by lazy {
    MutableLiveData<SomeState>()
  }
}

// This is top-level composable, it wont be recomposed ever
@Composable
fun MyComposableScreen(
viewModel:MyVm,
launchActivity:()->Unit
){
  val someDialog = remember { mutableStateOf(false) }

  MyComposableContent()

  GenericErrorDialog(someDialog = someDialog)

  when (val state = viewModel.response.observeAsState().value) {
    // Query 1  
    is Content -> LaunchedEffect(Unit) { launchActivity() }
    Error -> {
      // Query 2
  // Gets called everytime this composable gets invalidated, for eg in case of TextField change, compiler is invalidating it.
 // But if i change it to LaunchedEffect(Unit), invalidation has no effect,LaunchedEffect only gets called when there is new update to the LiveData. why?
      SideEffect { someDialog.value = true}
    }
  }
}

// This is the content, which can be recomposed in case of email is changed
@Composable
fun MyComposableContent(
onEmailChange:(email) -> Unit,
email:String,
){
  TextField(
   email = email,
   onValueChange = onEmailChange
  )
}

I have doubts related to Query 1 and Query 2 both are part of top-level composable which will never be re-composed, but can be invalidated,

when (val state = viewModel.response.observeAsState().value) { // observing to live-data
        // Query 1  
        is Content -> LaunchedEffect(Unit) { launchActivity() }
        Error -> {
          // Query 2
          SideEffect { someDialog.value = true}
        }
      }

In case of is

Content -> LaunchedEffect(Unit) { launchActivity() }

I believe this should be fine as we want to launch an activity only when LaunchedEffect is part of the first time composition, and it will be only part of the composition if live data state is Content

I faced issue in second scenario,

Error -> {
   // Query 2
  SideEffect { someDialog.value = true // shows a dialog} 
}

If last state of live-data, is Error in viewModel. And every time i make changes in the TextField my top level MyComposableScreen was getting invalidated(not recomposed) by compose compiler, and since last state of live-data was set as error, SideEffect was running every time, which is fine as it should run for every successful composition and re-composition.

But, if i change it from SideEffect to LaunchedEffect(Unit){someDialog.value = true} dialog box was not showing up every time MyComposableScreen was invalidated, thats the desired behavior.

LaunchedEffect(Unit) gets called only if there live-data emits the new state again because of any UI-action.

But, I am not sure regarding the reasoning behind it, why the code inside LaunchedEffect(Unit){someDialog.value = true} does not trigger after composable gets invalidated but the code inside SideEffect gets triggered after composable gets invalidated?


To make it more clear

I understand the difference

SideEffect -> on every successful composition and re-composition, if it's part of it
LaunchedEffect -> when its enters composition and span across re-composition unless the keys are changed.

But in above scenario - this code particularly

@Composable
fun MyTopLevelComposable(viewModel:MyViewModel){

  when (val state = viewModel.response.observeAsState().value) { // observing live-data state
    is Content -> LaunchedEffect(Unit) { launchActivity() }
    Error -> SideEffect { someDialog.value = true}
  }
}

It will never get recomposed. The only reason for this composable to be called again could be if compose compiler invalidates the view.

My Query is -> when view/composable gets invalidated

SideEffect {someDialog.value = true} executes, because it will again go through composition not re-composition as viewModel.response(which is live-data) last state was Error

But if change it to LaunchedEffect(Unit) {someDialog.value = true} it doesn't executes again after the composable is invalidated. It only reacts to a new state emitted by the live-data.

Question is why? Invalidate should start composition again, and since it's a composition. not re-composition LaunchedEffect should behave similarly to SideEffect in this scenario, as both are reacting to composition.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

书间行客 2025-01-18 23:33:21

在 Compose 中,不存在使视图无效这样的事情。

当您将 when 保持在与状态变量相同的作用域时,更改状态变量会重新组合 when 的内容,但是当您将其移动到单独的可组合项时,仅更新viewModel.response 可以重构它 - Compose 尝试尽可能减少要重构的视图数量。

LaunchedEffect(Unit) 将在两种情况下重新运行:

  1. 如果在之前的重组过程中将其从视图树中删除,然后再次添加。例如,如果将 LaunchedEffect 包装在 if 中,并且条件首先为 false,然后为 true。或者,在您的情况下,如果 whenis Content -> 之后选择 Error ->,这也将删除 LaunchedEffect 来自视图树。
  2. 如果传递给 LaunchedEffect 的键之一已更改。

看来您的问题是 LaunchedEffect 在新内容值传入时不会重新启动,要解决此问题,您需要在 LaunchedEffect< 中将此值作为 key 传递/code>,而不是 Unit

LaunchedEffect(state) { launchActivity() }

In Compose, there is no such thing as invalidating a view.

When you keep your when in the same scope as the state variable, changing the state variable recomposes the contents of when, but when you move it to a separate composable, only updating viewModel.response can recompose it - Compose tries to reduce the number of views to recompose as much as possible.

LaunchedEffect(Unit) will be re-run in two cases:

  1. If it was removed from the view tree during one of the previous recompositions and then added again. For example, if you wrap LaunchedEffect in if and the condition is first false and then true. Or, in your case, if when will choose Error -> after is Content ->, this will also remove LaunchedEffect from the view tree.
  2. If one of the keys passed to LaunchedEffect has changed.

It looks like your problem is that LaunchedEffect does not restart when new content value come in, to solve this, you need to pass this value as key in LaunchedEffect, instead of Unit:

LaunchedEffect(state) { launchActivity() }
如痴如狂 2025-01-18 23:33:21

他们的行为不同只是有原因的。查看文档

对于 LaunchEffect,它只会在第一次被调用,因为您为其键指定了 Unit。如果您希望它在特定的重组时触发,请使用您想要观察的状态值。每次更改时,都会触发 LaunchEffect

They just behave differently for a reason. Have a look at the documentation.

For LaunchEffect, it only gets call the first time because you've specified Unit for its key. If you'd like it to trigger at a specific recomposition, use the state value you'd like to observe. Each time it changes, the LaunchEffect will be triggered.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文