我正在Kotlin编写一个Android Jetpack Compose移动应用程序。我在郊游方面遇到了很多问题。我使用的是共享的首选项,所以我需要在协同程序范围内调用那些挂起的函数。我是新来的郊游,我显然不太了解它们。
我有一个选项,我正在保存在Android数据存储中,以便在应用程序启动时绕过启动屏幕。我在启动屏幕上还有一个复选框,从现在起可以绕过屏幕。我需要能够在启动屏幕显示时选中该选项,但如果确实显示了,也可以允许更改复选框。我最终想要一份汉堡菜单来设置这个和其他选项。由于DataStore操作是异步的,所以我不得不处理挂起例程
这是我的来电者,我正试图在这里决定是否绕过启动屏幕:
var shouldBypassSplashScreen: Boolean = false
runBlocking {
val job = launch {
shouldBypassSplashScreen = mainViewModel.isBypassSplashScreenOptChecked()
}
job.join()
}
LogHelper.logDebug("${MODULE_NAME}.$FUNCTION_NAME: shouldBypassSplashScreen = $shouldBypassSplashScreen")
这是被调用的函数,在我的MainViewModel
中(对不起,下面的粗体字不起作用,但我添加了下面的前两行):
**
var bypassSplashScreenOpt: MutableLiveData<Boolean>
= MutableLiveData(false)
private set
**
...
suspend fun isBypassSplashScreenOptChecked(): Boolean {
val FUNCTION_NAME = "isBypassSplashScreenOptChecked()"
viewModelScope.launch {
userPrefRepo.shouldBypassSplashScreen().collect {
withContext(Dispatchers.Main) {
LogHelper.logDebug("$MODULE_NAME.$FUNCTION_NAME: bypassSplashScreenOpt.value = ${bypassSplashScreenOpt.value}, it = $it")
bypassSplashScreenOpt.value = it
}
}
}
return bypassSplashScreenOpt.value!!
}
我尝试过很多不同的方法,但无论我做什么,调用函数后的日志消息总是显示在被调用函数中的日志消息之前。问题是,我的boolean在调用函数中总是false,因为它似乎不会等到调用程序运行。
===============
更新:
首先,感谢你们回答了我的问题。我现在意识到我对郊游的了解是多么的少。很明显,我需要仔细阅读这些建议,并努力思考。现在,即使是看你的建议,我也有点力不从心。
为了尽量不在我的原始帖子中放太多代码,我意识到我没有给出足够的上下文来说明我要做的事情。所以我更新了上面的原始帖子,新信息在粗体文本中。
我在这里真正想做的就是在DataStore中有一些选项,我可以在屏幕上设置这些选项,并检查我的逻辑。由于从DataStore读取数据是一个异步事件,所以我只需要能够等到它取回数据后才能检查、显示或设置数据。这看起来不应该那么复杂,但很明显。
我根据Tenfour04的初始响应更改了MainViewModel中的函数,该部分工作正常。
suspend fun isBypassSplashScreenOptChecked(): Boolean {
return userPrefRepo.shouldBypassSplashScreen().first()
}
从我读到的所有内容来看,我完全同意你的评论,即我不应该使用runBlocking
。然而,我完全不明白为什么会编译:
runBlocking {
launch {
shouldBypassSplashScreen = mainViewModel.isBypassSplashScreenOptChecked()
}
}
但这告诉我,当我编译launch
时,它是未定义的:
launch {
shouldBypassSplashScreen = mainViewModel.isBypassSplashScreenOptChecked()
}
这确实是我使用runBlocking
的唯一原因,因为没有它我就无法克服编译错误
isBypassSplashScreenOptChecked()
函数的问题在于,它不执行任何挂起的工作,而是异步地启动一些新的、不相关的协程,不等待该协程,只在其他协程启动之前返回变量的当前值。
它是一个suspend函数,所以它只能在您调用它时获得流的最新值。完全收集它没有意义,所以对它调用first()
。
假设userPrefRepo.shouldBypassSplashScreen()
返回一个Flow并且它正常工作,那么这个函数的正确实现是:
suspend fun isBypassSplashScreenOptChecked(): Boolean {
return userPrefRepo.shouldBypassSplashScreen().first()
}
我不知道你在一旁搞什么bypassSplashScreenOpt
。如果你没有在其他地方使用它,就删除它。
您在Composable中的代码非常糟糕。您应该永远避免调用runBlocking
,尤其是在Composable中,因为这意味着每次进行重组时,它都会阻塞一些长时间运行的操作。
顺便说一句,你启动一个协同程序并立即join()
是没有意义的。该代码的正确实现(如果在可组合程序中使用runBlocking
不是一个糟糕的想法)应该是val shouldBypassSplashScreen = runBlocking { mainViewModel.isBypassSplashScreenOptChecked() }
因此,由于检索该值需要一段时间,并且您应该避免在作文中出现阻塞,因此可以使用一些不同的策略。这里只有一个建议:
由于这可能不会花超过一秒钟的时间,只需在你的可堆肥中显示一个空白屏幕,直到你决定是否显示启动屏幕。因此,你在没有阻塞的情况下完成了你的合成,主线程也不会被冻结,这会让你的应用程序和设备看起来很糟糕。
当它第一次组成你的入口点可组合时,使用一个启动的效果来运行这个。当重组发生时,没有必要重新运行它。首先,这个组合中没有任何东西会导致它重新组合,但同时,你只是在等待第一个值到达,然后离开这个组合。
LaunchedEffect(Unit) {
if (userPrefRepo.shouldBypassSplashScreen()) {
navController.nagivate("main")
else {
navController.nagivate("splash")
}
}
// Just don't have anything displayed in this entry point composable.
我想我有一个解决方案可以修复这个错误。您在日志中只看到默认状态值,因为您试图直接在组合上下文中启动协程,而不是使用效果处理程序。
让我们从更改ViewModel中的代码开始:
// A mutable state flow holding the value.
private val _isBypassSplashScreenOptChecked = MutableStateFlow(false)
val isBypassSplashScreenOptChecked = _isBypassSplashScreenOptChecked.asStateFlow()
// Observe the value from the repository.
private fun getBypassSplashScreenOptChecked() {
viewModelScope.launch(Dispatchers.IO) {
userPrefRepo.shouldBypassSplashScreen().collect { isOptChecked ->
// Update the state value
withContext(Dispatchers.Main) {
LogHelper.logDebug("$MODULE_NAME shouldBypassSplashScreen: $isOptChecked")
_isBypassSplashScreenOptChecked.update { isOptChecked }
}
}
}
}
// Start observing the value from the repository
// when the ViewModel is created.
init {
getBypassSplashScreenOptChecked()
}
最后是可组合:
// Collect the state value from the ViewModel
val isBypassSplashScreenOptChecked by viewModel.isBypassSplashScreenOptChecked.collectAsState()
// Log the value whenever it changes
LaunchedEffect(key1 = isBypassSplashScreenOptChecked) {
LogHelper.logDebug("$MODULE_NAME shouldBypassSplashScreen: $isBypassSplashScreenOptChecked")
}
要了解有关Compose中状态的更多信息,您可以访问此链接。