如何在 Android 中管理依赖的 Kotlin 协程
How to manage dependent Kotlin coroutines in Android
我的用例如下:
假设有一个 Android 片段允许用户在商店中搜索杂货。有一个搜索视图,当他们键入时,新查询将发送到杂货商品网络服务,以询问哪些商品与查询匹配。成功后,查询 returns 包含产品名称、价格和营养信息的杂货项目列表。
在本地 Android 设备上,原始文件中存储了一份已知的“待售物品”列表。它在 raw
资源目录中,只是杂货品名称的列表,没有别的。
我们希望实现的行为是,当用户搜索商品时,系统会向他们显示与其查询匹配的商品列表以及“待售”商品上的视觉徽章
我试图满足的约束如下:
当用户加载 Android 片段时,我想使用 IO Dispatcher 使用 Kotlin 协程异步解析原始文本文件。解析后,项目将插入房间数据库 table 以获取“待售项目”,这只是一个名称列表,其中名称是主键。此列表可能为空,也可能很大(即 >10,0000)。
并行且独立于#1,当用户键入并进行不同的查询时,我想向服务器发送网络请求以检索与其查询匹配的杂货项目。当查询成功返回时,这些项目被插入到杂货项目
的房间数据库中的不同 table
最后,一旦我知道#1 的文本文件已被成功解析,我只想呈现从#2 返回的列表。一旦我知道 #1 已被成功解析,我想在名称上加入数据库中的 tables 并将该 LiveData 提供给我的 ViewModel 以呈现列表。如果#1 或#2 失败,我希望为用户提供“发生错误,重试”按钮
我现在挣扎的地方:
似乎可以通过在 ViewModel init 中启动一个使用 IO Dispatcher 的协程来实现。这样我每次创建 ViewModel 时只尝试解析一次文件(如果用户终止并重新打开应用程序,我可以重新解析它)
似乎可以通过使用另一个 IO Dispatcher 协程 + Retrofit + Room 来实现。
满足“仅在#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()
)
}
我的用例如下:
假设有一个 Android 片段允许用户在商店中搜索杂货。有一个搜索视图,当他们键入时,新查询将发送到杂货商品网络服务,以询问哪些商品与查询匹配。成功后,查询 returns 包含产品名称、价格和营养信息的杂货项目列表。
在本地 Android 设备上,原始文件中存储了一份已知的“待售物品”列表。它在 raw
资源目录中,只是杂货品名称的列表,没有别的。
我们希望实现的行为是,当用户搜索商品时,系统会向他们显示与其查询匹配的商品列表以及“待售”商品上的视觉徽章
我试图满足的约束如下:
当用户加载 Android 片段时,我想使用 IO Dispatcher 使用 Kotlin 协程异步解析原始文本文件。解析后,项目将插入房间数据库 table 以获取“待售项目”,这只是一个名称列表,其中名称是主键。此列表可能为空,也可能很大(即 >10,0000)。
并行且独立于#1,当用户键入并进行不同的查询时,我想向服务器发送网络请求以检索与其查询匹配的杂货项目。当查询成功返回时,这些项目被插入到杂货项目
的房间数据库中的不同 table最后,一旦我知道#1 的文本文件已被成功解析,我只想呈现从#2 返回的列表。一旦我知道 #1 已被成功解析,我想在名称上加入数据库中的 tables 并将该 LiveData 提供给我的 ViewModel 以呈现列表。如果#1 或#2 失败,我希望为用户提供“发生错误,重试”按钮
我现在挣扎的地方:
似乎可以通过在 ViewModel init 中启动一个使用 IO Dispatcher 的协程来实现。这样我每次创建 ViewModel 时只尝试解析一次文件(如果用户终止并重新打开应用程序,我可以重新解析它)
似乎可以通过使用另一个 IO Dispatcher 协程 + Retrofit + Room 来实现。
满足“仅在#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()
)
}