防止LaunchedEffect在配置更改时重新运行

10

我希望在可组合项加载时只运行一次代码。因此,我使用了具有键为true的LaunchedEffect来实现这一点。

LaunchedEffect(true) {
    // do API call
}

这段代码目前能够正常运行,但当屏幕方向发生变化等配置更改时会再次执行。如何避免在配置更改的情况下再次运行?

使用ViewModel,这是Jetpack Compose库中的一个。通过ViewModel,您可以执行网络请求,ViewModel将处理所有配置更改。 - d-feverx
这个回答解决了你的问题吗?Jetpack Compose如何在屏幕方向改变时保存状态 - Sergei S
3个回答

13
最简单的解决方案是存储关于您是否使用rememberSaveable进行API调用的信息:当配置更改时,它将保持不变。
var initialApiCalled by rememberSaveable { mutableStateOf(false) }
if (!initialApiCalled) {
    LaunchedEffect(Unit) {
        // do API call
        initialApiCalled = false
    }
}

这种解决方案的缺点在于,如果在API调用完成之前配置发生更改,则LaunchedEffect协程将被取消,您的API调用也将被取消。
最清晰的解决方案是使用视图模型,并在init内执行API调用:
class ScreenViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            // do API call
        }
    }
}

@Composable
fun Screen(viewModel: ScreenViewModel = viewModel()) {
    
}

建议像这样将视图模型作为参数传递, 官方文档。在生产代码中,您无需向此视图传递任何参数,只需像 Screen() 一样调用即可:视图模型将默认创建 viewModel() 参数。如 此答案 所示,它被移至测试/预览功能的参数。


1
@AndroidDev 这种方法是由官方文档推荐的。在生产代码中,您无需向此视图传递任何参数,调用viewModel()即可创建视图模型。如此答案所示,将其移至测试/预览能力的参数中。 - Phil Dukhov
1
Google最近明确表示了这一点。而且这是一个非常糟糕的想法。我肯定不会使用它。正如已经说明的那样,传递一个viewmodel会使你的组合部件变得不太可重用。我将坚持将其提升到另一个组合部件中。不要跟随或信任Google发布的所有内容。他们有很多缺乏经验的初级开发人员。 - Johann
1
@AndroidDev 如果您认为可以改进文档,可以使用您的建议创建问题 - Phil Dukhov
1
@AndroidKotlinNoob 你可以在你的视图模型中添加 SavedStateHandle 参数,参考这个答案 - Phil Dukhov
1
@Johann 请先制作Compose教程。 - user924
显示剩余6条评论

1
我认为最好的方法是在livedata/stateflow的延迟创建上使用.also,这样你就可以保证只要view model还活着,loadState只会被调用一次,并且也保证服务本身不会被调用,除非有人在监听它。然后你从viewmodel监听状态,不需要从启动的效果中调用任何api调用,而且你的代码将会对特定状态做出反应。
这里是一个代码示例
class MyViewModel : ViewModel() {
private val uiScreenState: : MutableStateFlow<WhatEverState> =
    MutableStateFlow(WhatEverIntialState).also {
        loadState()
    }

fun loadState(): StateFlow<WhatEverState>> {
    return users
}

private fun loadUsers() {
    // Do an asynchronous operation to fetch users.
}
}

当使用此代码时,您不需要在活动中调用loadstate,只需监听观察者即可。
您可以检查下面的代码以进行监听。
class MyFragment : Fragment {
override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    return ComposeView(requireContext()).apply {
        setContent {
            StartingComposeTheme {
                Box(modifier = Modifier.fillMaxSize()) {
                    val state by viewModel.uiScreenState.collectAsState()
                    when (state) {
                        //do something
                    }
                }
            }
        }
    }
}

}}


0

@Islam Mansour 的答案适用于将专用的 viewModel 与 UI 相关联,但我的情况是多个 UI 片段共享 ViewModel。

在我的情况下,上述答案无法解决我在用户导航到相关 UI 部分时仅调用 API 一次的问题。

因为我在 NavHost 中有多个可组合的 UI,作为 Fragment

而我的 ViewModel 贯穿所有片段。

因此,API 应该仅在用户导航到所需片段时调用。

因此,下面的惰性属性初始化程序解决了我的问题;

val myDataList by lazy {
    Log.d("test","call only once when called from UI used inside)")
    loadDatatoThisList()
    mutableStateListOf<MyModel>()
}

mutableStateListOf<LIST_TYPE> 会在数据添加到其中时自动重新组合UI。

使用by lazy修饰的变量只有在显式调用时才会被初始化一次。


拥有 sharedViewModel 不会阻止您为您的特定片段需求使用另一个 ViewModel,您仍然可以使用以下解决方案来启动您的内容。 - Islam Mansour

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接