简体   繁体   English

带有房间和状态处理的 Kotlin Coroutines Flow

[英]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对象呈现给ViewViewModel的责任。 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 ;我没有看到返回Flowfirst()版本; 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 曾经有一个返回Observablefirst()版本,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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM