如何在 Android 中管理依赖的 Kotlin 协程

How to manage dependent Kotlin coroutines in Android

我的用例如下:

假设有一个 Android 片段允许用户在商店中搜索杂货。有一个搜索视图,当他们键入时,新查询将发送到杂货商品网络服务,以询问哪些商品与查询匹配。成功后,查询 returns 包含产品名称、价格和营养信息的杂货项目列表。

在本地 Android 设备上,原始文件中存储了一份已知的“待售物品”列表。它在 raw 资源目录中,只是杂货品名称的列表,没有别的。

我们希望实现的行为是,当用户搜索商品时,系统会向他们显示与其查询匹配的商品列表以及“待售”商品上的视觉徽章


我试图满足的约束如下:

  1. 当用户加载 Android 片段时,我想使用 IO Dispatcher 使用 Kotlin 协程异步解析原始文本文件。解析后,项目将插入房间数据库 table 以获取“待售项目”,这只是一个名称列表,其中名称是主键。此列表可能为空,也可能很大(即 >10,0000)。

  2. 并行且独立于#1,当用户键入并进行不同的查询时,我想向服务器发送网络请求以检索与其查询匹配的杂货项目。当查询成功返回时,这些项目被插入到杂货项目

    的房间数据库中的不同 table
  3. 最后,一旦我知道#1 的文本文件已被成功解析,我只想呈现从#2 返回的列表。一旦我知道 #1 已被成功解析,我想在名称上加入数据库中的 tables 并将该 LiveData 提供给我的 ViewModel 以呈现列表。如果#1 或#2 失败,我希望为用户提供“发生错误,重试”按钮


我现在挣扎的地方:

  1. 似乎可以通过在 ViewModel init 中启动一个使用 IO Dispatcher 的协程来实现。这样我每次创建 ViewModel 时只尝试解析一次文件(如果用户终止并重新打开应用程序,我可以重新解析它)

  2. 似乎可以通过使用另一个 IO Dispatcher 协程 + Retrofit + Room 来实现。

  3. 满足“仅在#1 和#2 都完成时才向 ViewModel 提供数据,否则显示错误按钮”是这里的棘手部分。我如何公开 LiveData/Flow/something else?来自满足这些约束的存储库?

您可以通过在两个任务完成时让 ViewModel 进行监视并设置加载状态 LiveData 变量以指示 UI 仅应在两个任务完成后更新来实现此目的。例如:

class MainViewModel : ViewModel() {

    private var completedA = false
    private var completedB = false

    private val dataALiveData = MutableLiveData("")
    val dataA: LiveData<String>
        get() = dataALiveData

    private val dataBLiveData = MutableLiveData("")
    val dataB: LiveData<String>
        get() = dataBLiveData

    private val dataIsReadyLiveData = MutableLiveData(false)
    val dataIsReady: LiveData<Boolean>
        get() = dataIsReadyLiveData

    // You can trigger a reload of some of this data without having to reset
    // any flags - the UI will be updated when the task is complete
    fun reloadB() {
        viewModelScope.launch { doTaskB() }
    }

    private suspend fun doTaskA() {
        // Fake task A - once it's done post relevant data
        // (if applicable), indicate that it is completed, and
        // check if the app is ready
        delay(3200)
        dataALiveData.postValue("Data A")
        completedA = true
        checkForLoaded()
    }

    private suspend fun doTaskB() {
        // Fake task B - once it's done post relevant data
        // (if applicable), indicate that it is completed, and
        // check if the app is ready
        delay(2100)
        dataBLiveData.postValue("Data B")
        completedB = true
        checkForLoaded()
    }

    private fun checkForLoaded() {
        if( completedA && completedB ) {
            dataIsReadyLiveData.postValue(true)
        }
    }

    // Launch both coroutines upon creation to start loading
    // the two data streams
    init {
        viewModelScope.launch { doTaskA() }
        viewModelScope.launch { doTaskB() }
    }
}

activity 或 fragment 可以观察这三组 LiveData 以确定显示什么以及何时显示,例如隐藏显示的元素并显示进度条或加载指示器,直到完成加载。

如果你想处理错误状态,你可以让 dataIsReady LiveData 保存一个枚举或字符串来指示“正在加载”、“已加载”或“错误”。

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    val model: MainViewModel by viewModels()

    binding.textA.visibility = View.INVISIBLE
    binding.textB.visibility = View.INVISIBLE
    binding.progressBar.visibility = View.VISIBLE

    model.dataA.observe(this) { data ->
        binding.textA.text = data
    }

    model.dataB.observe(this) { data ->
        binding.textB.text = data
    }

    // Once the data is ready - change the view visibility state
    model.dataIsReady.observe(this) { isReady ->
        if( isReady ) {
            binding.textA.visibility = View.VISIBLE
            binding.textB.visibility = View.VISIBLE
            binding.progressBar.visibility = View.INVISIBLE

            // alternately you could read the data to display here
            // by calling methods on the ViewModel directly instead of
            // having separate observers for them
        }
    }
}

当您启动协程时,它们 return 一个您可以在另一个协程中等待的作业对象。因此,您可以为 1 启动一个作业,而 3 可以在开始连接表的流程之前等待它。

使用 Retrofit 和 Room 时,您可以使用 suspend 函数定义 Room 和 Retrofit DAOs/interfaces。这会导致他们生成内部使用适当线程的实现并挂起(不要 return)直到 inserting/updating/fetching 的工作完成。这意味着您知道当您的协程完成时,数据已经写入数据库。这也意味着您对 2 使用哪个调度程序并不重要,因为您不会调用任何阻塞函数。

对于1,如果解析是一个繁重的操作,Dispatchers.Default比Dispatchers.IO更合适,因为这项工作确实会占用CPU个核心。

如果您希望能够查看来自 1 的作业是否有错误,那么您实际上需要使用 async 而不是 launch,以便在您等待时重新抛出任何抛出的异常它在协程中。

3 可以是来自 Room 的流(因此您可以使用 DAO 中的连接定义查询),但您可以将其包装在等待 1 的 flow 构建器中。它可以 return 结果,其中包含数据或错误,因此 UI 可以显示错误状态。

2 可以独立运行,只需通过让用户输入调用 ViewModel 函数来写入 Room 数据库即可。 3使用的repository flow会在数据库变化时自动拾取变化。

下面是实现此任务的 ViewModel 代码示例。

private val parsedTextJob = viewModelScope.async(Dispatchers.Default) {
    // read file, parse it and write to a database table
}

val theRenderableList: SharedFlow<Result<List<SomeDataType>>> = flow {
    try {
        parsedTextJob.await()
    } catch (e: Exception) {
        emit(Result.failure(e)
        return@flow
    }
    emitAll(
        repository.getTheJoinedTableFlowFromDao()
            .map { Result.success(it) }
    )
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)

fun onNewUserInput(someTextFromUser: String) {
    viewModelScope.launch {
        // Do query from Retrofit.
        // Parse results and write to database.
    }
}

如果您更喜欢 LiveData 而不是 SharedFlow,您可以将上面的 theRenderableList 替换为:

val theRenderableList: LiveData<Result<List<SomeDataType>>> = liveData {
    try {
        parsedTextJob.await()
    } catch (e: Exception) {
        emit(Result.failure(e)
        return@liveData
    }
    emitSource(
        repository.getTheJoinedTableFlowFromDao()
            .map { Result.success(it) }
            .asLiveData()
    )
}