Kotlin поток сопрограмм с обработкой номеров и состояний - PullRequest
0 голосов
/ 28 января 2020

Я испытываю поток новой сопрограммы, моя цель - создать простой репозиторий, который может извлекать данные из веб-API и сохранять их в БД, а также возвращать поток из БД.

I Я использую room и firebase в качестве веб-API, теперь все выглядит довольно просто, пока я не попытаюсь передать ошибки, поступающие из API в пользовательский интерфейс.

Поскольку я получаю поток из базы данных, которая содержит только данные и без состояния, каков правильный подход, чтобы придать ему состояние (например, загрузка, содержимое, ошибка), комбинируя его с результатом веб-API?

Часть кода, который я написал:

DAO:

@Query("SELECT * FROM users")
fun getUsers(): Flow<List<UserPojo>>

Репозиторий:

val users: Flow<List<UserPojo>> = userDao.getUsers()

Вызов 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))
    }
}

Надеюсь, вопрос достаточно ясен Спасибо за помощь

Редактировать: Поскольку вопрос не ясен, я попытаюсь прояснить. Моя проблема в том, что с потоком по умолчанию, генерируемым комнатой, у вас есть только данные, поэтому, если бы я подписался на поток, я бы только получил данные (например, в этом случае я бы только получил список пользователей). То, что мне нужно достичь, это какой-то способ уведомления о состоянии приложения, например, загрузка или ошибка. На данный момент я могу думать только об объекте «ответа», содержащем состояние, но я не могу найти способ его реализовать.

Что-то вроде:

fun getUsers(): Flow<Lce<List<UserPojo>>>{
    emit(Loading())
    downloadFromApi()
    if(downloadSuccessful)
        return flowFromDatabase
    else
        emit(Error(throwable))
}

Но очевидная проблема, с которой я сталкиваюсь, заключается в том, что поток из базы данных имеет тип Flow<List<UserPojo>>, я не знаю, как «обогатить» его состоянием, редактирующим поток, без потери подписки из базы данных и без запуска нового сетевого вызова каждый раз, когда база данных обновляется (делая это в преобразовании карты).

Надеюсь, что это понятнее

Ответы [ 2 ]

2 голосов
/ 29 января 2020

Я считаю, что это больше вопрос архитектуры, но позвольте мне сначала попытаться ответить на некоторые из ваших вопросов.

Моя проблема заключается в том, что при использовании потока по умолчанию для комнаты у вас есть только данные поэтому, если бы я подписался на поток, я бы только получил данные

Если есть ошибка с Flow, возвращаемым Room, вы можете обработать ее через catch()

Что мне нужно сделать, так это каким-то образом уведомить о состоянии приложения, например о загрузке или ошибке.

Я согласен с вами, что наличие State объект - это хороший подход. На мой взгляд, ViewModel является обязанностью представить State объект View. У этого State объекта должен быть способ выявлять ошибки.

В настоящий момент я могу думать только о том, как объект «ответа», содержащий состояние, но я не могу найти способ реализовать его.

Я обнаружил, что легче иметь объект State, чтобы элементы управления ViewModel отвечали за ошибки, а не объект, всплывающий из Service layer.

Теперь, когда эти вопросы не нужны, позвольте мне попытаться предложить одно конкретное «решение» вашей проблемы.

Как вы упоминаете, это обычная практика Repository, который обрабатывает получение данных из нескольких источников данных. В этом случае Repository будет принимать DAO и объект, представляющий получение данных из сети, назовем его Api. Я предполагаю, что вы используете FirebaseFirestore, поэтому сигнатура класса и метода будет выглядеть примерно так:

class Api(private val firestore: FirebaseFirestore) {

fun getUsers() : Flow<List<UserApiPojo>

}

Теперь возникает вопрос, как превратить API на основе обратного вызова в Flow. К счастью, мы можем использовать callbackFlow() для этого. Тогда 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))
    }
  }
}

Как вы можете видеть, callbackFlow позволяет нам отменить поток, когда что-то идет не так, и заставить кого-то donwnstream обработать ошибку.

Переход к Repository Теперь мы хотели бы сделать что-то вроде:

val users: Flow<List<User>> = Flow.concat(userDao.getUsers().toUsers(), api.getUsers().toUsers()).first()

Здесь есть несколько предостережений. first() и concat() - это операторы, которые вам придётся придумать. Я не видел версию first(), которая возвращает Flow; это оператор терминала (Rx имел версию first(), которая вернула Observable, Дэн Лью использует ее в этой записи). Flow.concat(), похоже, тоже не существует. Цель users - вернуть Flow, который испускает первое значение, испускаемое любым источником Flows. Также обратите внимание, что я сопоставляю пользователей DAO и пользователей Api с общим User объектом.

Теперь мы можем поговорить о ViewModel. Как я уже говорил, у ViewModel должно быть что-то, что содержит State. Это State должно представлять данные, ошибки и состояния загрузки. Один из способов сделать это - использовать класс данных.

data class State(val users: List<User>, val loading: Boolean, val serverError: Boolean)

Поскольку у нас есть доступ к Repository, ViewModel может выглядеть следующим образом:

val state = repo.users.map {users -> State(users, false, false)}.catch {emit(State(emptyList(), false, true)}

Пожалуйста, имейте в виду, что это грубое объяснение, указывающее вам направление, есть много способов выполнить sh управление состоянием, и это ни в коем случае не полная реализация. Может даже не иметь смысла превращать вызов API в Flow, например.

0 голосов
/ 30 января 2020

Ответ от Эммануэля очень близок к ответу на то, что мне нужно, мне нужно кое-что прояснить.

Возможно, даже не имеет смысла превращать вызов API в поток

Вы абсолютно правы, на самом деле я хочу сделать это сопрограммой, мне не нужно, чтобы это был поток.

Если есть ошибка с потоком, возвращаемым Room, вы можете обработать его с помощью catch ()

Да, я обнаружил это после публикации вопроса. Но моя проблема больше похожа на:

Я бы хотел вызвать метод, скажем, «getData», этот метод должен возвращать поток из БД, запустить сетевой вызов для обновления БД (так, чтобы я Я получу уведомление, когда это будет сделано через поток БД) и где-то здесь, мне нужно будет сообщить пользовательскому интерфейсу, если БД или сеть допустили ошибку, верно? Или я должен сделать отдельные «getDbFlow» и «updateData» и получить ошибки отдельно для каждого?

val пользователей: Flow> = Flow.concat (userDao.getUsers (). ToUsers ( ), api.getUsers (). toUsers ()). first ()

Это хорошая идея, но я бы хотел сохранить БД в качестве единственного источника правды и никогда не возвращаться к интерфейсу пользователя любые данные напрямую из сети

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...