You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何确保Kotlin挂起函数按序执行?解决重复执行问题

问题

应用逻辑如下:

  1. 应用首次启动时显示引导页,调用OnboardingViewModel;
  2. 需要按顺序完成三件事:
    • 通过preferencesRepository检查引导流程是否已完成;
    • 若未完成,按顺序执行两个挂起函数:
      1. 获取远程数据;
      2. 为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自然被调用两次。同时代码里还有两个明显问题:

  1. init块里错误使用了不存在的_showOnboarding.value,实际应该用_showOnboardingScreen.value;
  2. checkStatus是普通函数,如果preferencesRepository.readBool是挂起函数,直接调用会导致无法正确获取偏好值,甚至引发线程异常。

修复步骤

  1. 修正变量名错误:把if (_showOnboarding.value)替换为if (_showOnboardingScreen.value);
  2. 调整checkStatus为挂起函数:如果偏好设置的读写是挂起操作,修改checkStatus为suspend fun,确保能正确获取值;
  3. 标记引导完成:在所有引导操作执行完毕后,调用preferencesRepository.setBool(Settings.SHOW_ONBOARDING_SCREEN, false),避免后续重复执行引导流程;
  4. 排查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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 01:54:51