1

To increment the quantity in the Realtime Database, I can simply use:

override fun incrementQuantity() = flow {
    try {
        heroIdRef.update("quantity", FieldValue.increment(1)).await()
        emit(Result.Success(true))
    } catch (e: Exception) {
        emit(Result.Failure(e))
    }
}

And works well. The problem comes when I need to check the quantity first and then increment. The above solution doesn't help, so I need to use transactions. Here is what I've tried:

override fun incrementQuantity() {
    val transaction = object : Transaction.Handler {
        override fun doTransaction(mutableData: MutableData): Transaction.Result {
            val quantity = mutableData.getValue(Long::class.java) ?: return Transaction.success(mutableData)
            if (quantity == 1L) {
                mutableData.value = null
            } else {
                mutableData.value = quantity + 1
            }
            return Transaction.success(mutableData)
        }

        override fun onComplete(error: DatabaseError?, committed: Boolean, data: DataSnapshot?) {
            throw error.toException()
        }
    }
    heroIdRef.runTransaction(transaction)
}

And works, but I cannot see how use Kotlin Coroutines. I just want to call await() and return a flow, as in the first example. How can I do that?

Joan P.
  • 2,368
  • 6
  • 30
  • 63

1 Answers1

1

This isn't really a correct use of Flow in either case, because a Flow is for retrieving multiple things in series, but this only returns one thing. It is more suited to a suspend function that directly returns that thing.

Anyway, I'm not a Firebase user, so I might make a mistake here. It looks like their Kotlin library doesn't provide a suspend function version of running a transaction. You could write your own like this. It's slightly messy because the completion callback has three parameters, so we must either return a tuple or a wrapper class.

data class CompletedTransaction(val error: DatabaseError?, val committed: Boolean, val data: DataSnapshot?)

suspend fun DatabaseReference.runTransaction(
    fireLocalEvents: Boolean = true,
    action: (MutableData)->Transaction.Result
): CompletedTransaction = suspendCoroutine { continuation ->
    val handler = object : Transaction.Handler {
        override fun doTransaction(mutableData: MutableData): Transaction.Result =
            action(mutableData)

        override fun onComplete(error: DatabaseError?, committed: Boolean, data: DataSnapshot?) =
            continuation.resume(CompletedTransaction(error, committed, data))
    }
    runTransaction(handler, fireLocalEvents)
}

Then you could do:

override suspend fun incrementQuantity(): Result {
    val transaction = heroIdRef.runTransaction { mutableData ->
        val quantity = mutableData.getValue(Long::class.java) ?: return@runTransaction Transaction.success(mutableData)
        mutableData.value = if (quantity == 1L) null else quantity + 1
        Transaction.success(mutableData)
    }
    val failure = transaction.error?.toException()?.let { Result.Failure(it) }
    return failure ?: Result.Success(true)
}

If you are required to use a Flow for some reason, it would be like:

override suspend fun incrementQuantity() = flow {
    val transaction = heroIdRef.runTransaction { mutableData ->
        val quantity = mutableData.getValue(Long::class.java) ?: return@runTransaction Transaction.success(mutableData)
        mutableData.value = if (quantity == 1L) null else quantity + 1
        Transaction.success(mutableData)
    }
    val failure = transaction.error?.toException()?.let { Result.Failure(it) }
    emit(failure ?: Result.Success(true))
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • One more really quick question. If I have had a Result.Loading state, and inside the `incrementQuantity()` function I had to use `emit(Result.Loading)` before getting any other result, it would make sense to use a **flow**? – Joan P. Sep 16 '22 at 09:00
  • 1
    It's more sensible to use in that case, although I think it would be easier to do on the receiving end so you don't have to deal with a flow. For example calling in sequence: `showLoadingUi(); viewModel.incrementQuantity(); removeLoadingUi();` – Tenfour04 Sep 16 '22 at 12:49
  • Thank you. I just added a new [question](https://stackoverflow.com/questions/73754131/how-to-correctly-use-flow-and-progressbar-in-jetpack-compose) regarding this new confusion. If you have time, maybe you can take a look. – Joan P. Sep 17 '22 at 10:40
  • I just raised a bounty for this [question](https://stackoverflow.com/questions/73754131/how-to-correctly-use-flow-and-progressbar-in-jetpack-compose), because I got only one unsatisfying answer. Do you think you can help me with that? Thanks. – Joan P. Sep 19 '22 at 10:46
  • I'm sorry, I have very little experience with Compose so far, so I'm not a good resource for best practices advice on that. From a coroutines conventions standpoint, I think it's OK either way. If you were using the old imperative UI, I think it would be simpler without flows as my comment above describes. But in Compose, it might be simpler to use a flow with `collectAsState()`. @JoanP. – Tenfour04 Sep 19 '22 at 12:53
  • Hey. Maybe you can help me with [that](https://stackoverflow.com/questions/73853421/how-to-stop-collectaslazypagingitems-from-firing-when-itemcount-is-0) too. – Joan P. Sep 28 '22 at 11:59