如何确保Kotlin挂起函数按序执行?解决重复执行问题
问题
应用逻辑如下:
- 应用首次启动时显示引导页,调用
OnboardingViewModel; - 需要按顺序完成三件事:
- 通过
preferencesRepository检查引导流程是否已完成; - 若未完成,按顺序执行两个挂起函数:
- 获取远程数据;
- 为Room数据库中的所有元素分配日期。
- 通过
当前实现中,第二个挂起函数assignDates执行了两次,导致日期错误(必须有一个元素的日期为当前日期)。需要确保这些操作按顺序执行且仅执行一次。
相关代码
OnboardingViewModel
@HiltViewModel class OnboardingViewModel @Inject constructor( private val preferencesRepository: PreferencesRepository, private val repository: ItemRepository, private val unsplashRepository: UnsplashRepository ) : ViewModel() { private val _tag = "OnboardingVM" private val today = LocalDate.now() private val _showOnboardingScreen = MutableStateFlow(true) val showOnboardingScreen = _showOnboardingScreen.asStateFlow() private var _itemsWithoutDate = mutableStateOf<List<Item>>(listOf()) private var _itemsWithoutImage = mutableStateOf<List<Item>>(listOf()) init { viewModelScope.launch { checkStatus() if (_showOnboarding.value) { loadRemoteItems() assignDates() } } } private fun checkStatus() { _showOnboardingScreen.value = preferencesRepository.readBool(Settings.SHOW_ONBOARDING_SCREEN) ?: true Log.d(_tag, "Show onboarding: ${showOnboardingScreen.value}") } private suspend fun loadRemoteItems() { Log.d(_tag, "Begin loading remote items") val remoteHash = try { repository.getHash() } catch (e: Exception) { Log.d("Ktor", "Error getting remote hash: $e") "" } Log.d(_tag, "Remote hash: [$remoteHash]") val localHash = preferencesRepository.readString(Settings.LOCAL_HASH_STRING) Log.d(_tag, "Local hash: [$localHash]") if (remoteHash != "" && remoteHash != localHash) { Log.d(_tag, "Loading remote items") val remoteItems = try { repository.getAllRemote() } catch (e: Exception) { emptyList() } if (remoteItems.isNotEmpty()) { repository.insertAllRemote(remoteItems) preferencesRepository.setString( Settings.LOCAL_HASH_STRING, remoteHash ) preferencesRepository.setDate( Settings.LAST_UPDATE_CHECK_DATE, today.toEpochDay() ) Log.d(_tag, "Loaded [${remoteItems.size}] remote items.") } } } private suspend fun assignDates() { _itemsWithoutDate.value = repository.getItemsWithoutDate() Log.d(_tag, "Items without date before: ${_itemsWithoutDate.value.size}") if (_itemsWithoutDate.value.isNotEmpty()) { repository.updateItemDate(_itemsWithoutDate.value.shuffled()) } _itemsWithoutDate.value = repository.getItemsWithoutDate() Log.d(_tag, "Items without date after: ${_itemsWithoutDate.value.size}") } }
ItemRepository & DAO
// Repository suspend fun getItemsWithoutDate(): List<Item> { return dao.getItemsWithoutDate() } suspend fun updateItemDate(items: List<Item>) { val startDate = dao.getMaxDate()?.plusDays(1) ?: LocalDate.now() Log.d("DailyItemVM", "Repo. Updating items' dates. Start date: $startDate") var newDate: LocalDate for ((increment, item) in items.withIndex()) { newDate = LocalDate.from(startDate).plusDays(increment.toLong()) dao.updateDate(item.id, newDate) Log.d("DailyItemVM", "Repo. Updated date of item: ${item.id} to $newDate") } } // DAO @Transaction @Query("SELECT * FROM item WHERE NULLIF(date, '') IS NULL") suspend fun getItemsWithoutDate(): List<Item> @Query("UPDATE item SET date = :date WHERE id = :id") suspend fun updateDate(id: Int, date: LocalDate)
LogCat输出
OnboardingVM D Begin loading remote items OnboardingVM D Show onboarding: true OnboardingVM D Show onboarding: true OnboardingVM D Begin loading remote items OnboardingVM D Remote hash: [c0a7239382390c64f4416dc11d3c6a4a6fa84c7c3e5e57073b712dcd0fa32153] OnboardingVM D Local hash: [null] OnboardingVM D Loading remote items OnboardingVM D Loaded [204] remote items. OnboardingVM D Items without date before: 208 DailyItemVM D Repo. Updating items' dates. Start date: 2023-08-16 DailyItemVM D Repo. Updated date of item: 239 to 2023-08-16 ...(省略部分日志) OnboardingVM D Items without date after: 0
解决方案
问题根源
从日志里的重复打印可以看出,OnboardingViewModel被实例化了两次,导致init块里的引导流程执行了两遍,assignDates自然被调用两次。同时代码里还有两个明显问题:
init块里错误使用了不存在的_showOnboarding.value,实际应该用_showOnboardingScreen.value;checkStatus是普通函数,如果preferencesRepository.readBool是挂起函数,直接调用会导致无法正确获取偏好值,甚至引发线程异常。
修复步骤
- 修正变量名错误:把
if (_showOnboarding.value)替换为if (_showOnboardingScreen.value); - 调整
checkStatus为挂起函数:如果偏好设置的读写是挂起操作,修改checkStatus为suspend fun,确保能正确获取值; - 标记引导完成:在所有引导操作执行完毕后,调用
preferencesRepository.setBool(Settings.SHOW_ONBOARDING_SCREEN, false),避免后续重复执行引导流程; - 排查ViewModel重复实例化:检查UI层是否存在重复创建ViewModel的情况(比如导航重复触发、布局中多次绑定ViewModel)。
修正后的核心代码
init { viewModelScope.launch { checkStatus() if (_showOnboardingScreen.value) { loadRemoteItems() assignDates() // 标记引导流程完成,避免重复执行 preferencesRepository.setBool(Settings.SHOW_ONBOARDING_SCREEN, false) _showOnboardingScreen.value = false } } } // 若preferences的read操作是挂起函数,改为suspend函数 private suspend fun checkStatus() { _showOnboardingScreen.value = preferencesRepository.readBool(Settings.SHOW_ONBOARDING_SCREEN) ?: true Log.d(_tag, "Show onboarding: ${showOnboardingScreen.value}") }
额外优化
assignDates里可以省去更新后的数据库查询,因为我们已经拿到了所有无日期的条目,更新后必然全部有日期:
private suspend fun assignDates() { val itemsWithoutDate = repository.getItemsWithoutDate() Log.d(_tag, "Items without date before: ${itemsWithoutDate.size}") if (itemsWithoutDate.isNotEmpty()) { repository.updateItemDate(itemsWithoutDate.shuffled()) Log.d(_tag, "Items without date after: 0") } else { Log.d(_tag, "Items without date after: 0") } }
内容的提问来源于stack exchange,提问作者zkvvoob
相关产品推荐
相关产品推荐

