副作用问题 - Jetpack Compose 中的 LaunchedEffect 和 SideEffect
为什么每次我的可组合项失效时都会调用 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 itLaunchedEffect
-> 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
在 Compose 中,不存在使视图无效这样的事情。
当您将
when
保持在与状态变量相同的作用域时,更改状态变量会重新组合when
的内容,但是当您将其移动到单独的可组合项时,仅更新viewModel.response
可以重构它 - Compose 尝试尽可能减少要重构的视图数量。LaunchedEffect(Unit)
将在两种情况下重新运行:LaunchedEffect
包装在if
中,并且条件首先为false
,然后为true
。或者,在您的情况下,如果when
在is Content ->
之后选择Error ->
,这也将删除LaunchedEffect
来自视图树。LaunchedEffect
的键之一已更改。看来您的问题是
LaunchedEffect
在新内容值传入时不会重新启动,要解决此问题,您需要在LaunchedEffect< 中将此值作为
key
传递/code>,而不是Unit
: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 ofwhen
, but when you move it to a separate composable, only updatingviewModel.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:LaunchedEffect
inif
and the condition is firstfalse
and thentrue
. Or, in your case, ifwhen
will chooseError ->
afteris Content ->
, this will also removeLaunchedEffect
from the view tree.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 askey
inLaunchedEffect
, instead ofUnit
:他们的行为不同只是有原因的。查看文档。
对于
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 specifiedUnit
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, theLaunchEffect
will be triggered.