Home > Net >  How to increment a counter in Realtime Database using transactions with Kotlin Coroutines?
How to increment a counter in Realtime Database using transactions with Kotlin Coroutines?

Time:09-16

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?

CodePudding user response:

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))
}
  • Related