[英]Kotlin Coroutines Flow with Room and state handling
I'm trying out the new coroutine's flow, my goal is to make a simple repository that can fetch data from a web api and save it to db, also return a flow from the db.我正在尝试新的协程流程,我的目标是创建一个简单的存储库,该存储库可以从 Web api 获取数据并将其保存到 db,还可以从 db 返回一个流。
I'm using room and firebase as the web api, now everything seems pretty straight forward until i try to pass errors coming from the api to the ui.我使用 room 和 firebase 作为 web api,现在一切似乎都很简单,直到我尝试将来自 api 的错误传递给 ui。
Since i get a flow from the database which only contains the data and no state, what is the correct approach to give it a state (like loading, content, error) by combining it with the web api result?由于我从数据库中得到一个只包含数据而没有状态的流,那么通过将它与 web api 结果相结合来赋予它一个状态(如加载、内容、错误)的正确方法是什么?
Some of the code i wrote:我写的一些代码:
The DAO:道:
@Query("SELECT * FROM users")
fun getUsers(): Flow<List<UserPojo>>
The Repository:存储库:
val users: Flow<List<UserPojo>> = userDao.getUsers()
The Api call: Api 调用:
override fun downloadUsers(filters: UserListFilters, onResult: (result: FailableWrapper<MutableList<UserApiPojo>>) -> Unit) {
val data = Gson().toJson(filters)
functions.getHttpsCallable("users").call(data).addOnSuccessListener {
try {
val type = object : TypeToken<List<UserApiPojo>>() {}.type
val users = Gson().fromJson<List<UserApiPojo>>(it.data.toString(), type)
onResult.invoke(FailableWrapper(users.toMutableList(), null))
} catch (e: java.lang.Exception) {
onResult.invoke(FailableWrapper(null, "Error parsing data"))
}
}.addOnFailureListener {
onResult(FailableWrapper(null, it.localizedMessage))
}
}
I hope the question is clear enough Thanks for the help我希望问题足够清楚 谢谢你的帮助
Edit: Since the question wasn't clear i'll try to clarify.编辑:由于问题不清楚,我会尽力澄清。 My issue is that with the default flow emitted by room you only have the data, so if i were to subscribe to the flow i would only receive the data (eg. In this case i would only receive a list of users).
我的问题是,使用 room 发出的默认流,您只有数据,所以如果我要订阅流,我只会收到数据(例如,在这种情况下,我只会收到用户列表)。 What i need to achieve is some way to notify the state of the app, like loading or error.
我需要实现的是某种通知应用程序状态的方法,例如加载或错误。 At the moment the only way i can think of is a "response" object that contains the state, but i can't seem to find a way to implement it.
目前我能想到的唯一方法是包含状态的“响应”对象,但我似乎无法找到实现它的方法。
Something like:就像是:
fun getUsers(): Flow<Lce<List<UserPojo>>>{
emit(Loading())
downloadFromApi()
if(downloadSuccessful)
return flowFromDatabase
else
emit(Error(throwable))
}
But the obvious issue i'm running into is that the flow from the database is of type Flow<List<UserPojo>>
, i don't know how to "enrich it" with the state editing the flow, without losing the subscription from the database and without running a new network call every time the db is updated (by doing it in a map transformation).但是我遇到的一个明显问题是来自数据库的流是
Flow<List<UserPojo>>
,我不知道如何通过编辑流的状态来“丰富它”,而不会丢失来自数据库并且每次更新数据库时都不会运行新的网络调用(通过在地图转换中进行)。
Hope it's clearer希望它更清楚
I believe this is more of an architecture question, but let me try to answer some of your questions first.我相信这更像是一个架构问题,但让我先尝试回答您的一些问题。
My issue is that with the default flow emitted by room you only have the data, so if i were to subscribe to the flow i would only receive the data
我的问题是,使用房间发出的默认流量,您只有数据,所以如果我要订阅流量,我只会收到数据
If there is an error with the Flow
returned by Room, you can handle it via catch()
如果Room返回的
Flow
有错误,可以通过catch()
What i need to achieve is some way to notify the state of the app, like loading or error.
我需要实现的是某种通知应用程序状态的方法,例如加载或错误。
I agree with you that having a State
object is a good approach.我同意你的观点,拥有一个
State
对象是一种很好的方法。 In my mind, it is the ViewModel
's responsibility to present the State
object to the View
.在我看来,将
State
对象呈现给View
是ViewModel
的责任。 This State
object should have a way to expose errors.这个
State
对象应该有一种方法来暴露错误。
At the moment the only way i can think of is a "response" object that contains the state, but i can't seem to find a way to implement it.
目前我能想到的唯一方法是包含状态的“响应”对象,但我似乎无法找到实现它的方法。
I have found that it is easier to have the State
object that the ViewModel
controls be responsible for errors instead of an object that bubbles up from the Service
layer.我发现让
ViewModel
控件对错误负责的State
对象而不是从Service
层冒泡的对象更容易。
Now with these questions out of the way, let me try to propose one particular "solution" to your issue.现在解决这些问题,让我尝试为您的问题提出一个特定的“解决方案”。
As you mention, it is common practice to have a Repository
that handles retrieving data from multiple data sources.正如您提到的,通常的做法是拥有一个
Repository
来处理从多个数据源检索数据。 In this case, the Repository
would take the DAO
and an object that represents getting data from the network, let's call it Api
.在这种情况下,
Repository
将采用DAO
和一个代表从网络获取数据的对象,我们称之为Api
。 I am assuming that you are using FirebaseFirestore
, so the class and method signature would look something like this:我假设您正在使用
FirebaseFirestore
,因此类和方法签名将如下所示:
class Api(private val firestore: FirebaseFirestore) {
fun getUsers() : Flow<List<UserApiPojo>
}
Now the question becomes how to turn a callback based API into a Flow
.现在问题变成了如何将基于回调的 API 转换为
Flow
。 Luckily, we can use callbackFlow()
for this.幸运的是,我们可以为此使用
callbackFlow()
。 Then Api
becomes:然后
Api
变成:
class Api(private val firestore: FirebaseFirestore) {
fun getUsers() : Flow<List<UserApiPojo> = callbackFlow {
val data = Gson().toJson(filters)
functions.getHttpsCallable("users").call(data).addOnSuccessListener {
try {
val type = object : TypeToken<List<UserApiPojo>>() {}.type
val users = Gson().fromJson<List<UserApiPojo>>(it.data.toString(), type)
offer(users.toMutableList())
} catch (e: java.lang.Exception) {
cancel(CancellationException("API Error", e))
}
}.addOnFailureListener {
cancel(CancellationException("Failure", e))
}
}
}
As you can see, callbackFlow
allows us to cancel the flow when something goes wrong and have someone donwnstream handle the error.如您所见,
callbackFlow
允许我们在出现问题时取消流程,并让某个人 donwnstream 处理错误。
Moving to the Repository
we would now like to do something like:移动到
Repository
我们现在想要做这样的事情:
val users: Flow<List<User>> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
There are a few caveats here.这里有一些注意事项。
first()
and concat()
are operators you will have to come up with it seems. first()
和concat()
是您必须想出的运算符。 I did not see a version of first()
that returns a Flow
;我没有看到返回
Flow
的first()
版本; it is a terminal operator (Rx used to have a version of first()
that returned an Observable
, Dan Lew uses it in this post).它是一个终端操作符(Rx 曾经有一个返回
Observable
的first()
版本,Dan Lew 在这篇文章中使用了它)。 Flow.concat()
does not seem to exist either. Flow.concat()
似乎也不存在。 The goal of users
is to return a Flow
that emits the first value emitted by any of the source Flows
. users
的目标是返回一个Flow
,它发出任何源Flows
发出的第一个值。 Also, note that I am mapping DAO users and Api users to a common User
object.另外,请注意,我将 DAO 用户和 Api 用户映射到一个通用的
User
对象。
We can now talk about the ViewModel
.我们现在可以谈谈
ViewModel
。 As I said before, the ViewModel
should have something that holds State
.正如我之前所说,
ViewModel
应该有一些包含State
东西。 This State
should represent data, errors and loading states.此
State
应表示数据、错误和加载状态。 One way that can be accomplished is with a data class.可以实现的一种方法是使用数据类。
data class State(val users: List<User>, val loading: Boolean, val serverError: Boolean)
Since we have access to the Repository
the ViewModel
can look like:由于我们可以访问
Repository
因此ViewModel
可能如下所示:
val state = repo.users.map {users -> State(users, false, false)}.catch {emit(State(emptyList(), false, true)}
Please keep in mind that this is a rough explanation to point you in a direction, there are many ways to accomplish state management and this is by no means a complete implementation.请记住,这是为您指明方向的粗略解释,有很多方法可以完成状态管理,但这绝不是一个完整的实现。 It may not even make sense to turn the API call into a
Flow
, for example.例如,将 API 调用转换为
Flow
甚至可能没有意义。
The answer from Emmanuel is really close to answering what i need, i need some clarifications about some of it. Emmanuel 的回答非常接近于回答我的需要,我需要对其中的一些进行澄清。
It may not even make sense to turn the API call into a Flow
将 API 调用转换为 Flow 甚至可能没有意义
You are totally right, in fact i only want to actually make it a coroutine, i don't really need it to be a flow.你是完全正确的,事实上我只想让它成为一个协程,我真的不需要它成为一个流。
If there is an error with the Flow returned by Room, you can handle it via catch()
如果Room返回的Flow有错误,可以通过catch()处理
Yes i discovered this after posting the question.是的,我在发布问题后发现了这一点。 But my problem is more something like:
但我的问题更像是:
I'd like to call a method, say "getData", this method should return the flow from db, start the network call to update the db (so that i'm going to be notified when it's done via the db flow) and somewhere in here, i would need to let the ui know if db or network errored, right?.我想调用一个方法,比如“getData”,这个方法应该从数据库返回流,启动网络调用来更新数据库(这样我就会在通过数据库流完成时得到通知)和在这里的某个地方,我需要让用户界面知道数据库或网络是否出错,对吗? Or should i maybe do a separate "getDbFlow" and "updateData" and get the errors separately for each one?
或者我应该做一个单独的“getDbFlow”和“updateData”并分别获取每个错误?
val users: Flow> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
val users: Flow> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()
This is a good idea, but i'd like to keep the db as the single source of truth, and never return to the ui any data directly from the network这是一个好主意,但我想将数据库作为唯一的事实来源,并且永远不要直接从网络返回任何数据到用户界面
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.