kotlincoroutine

Kotlin multiple suspend cancellable coroutine


I have a problem with coroutines. I use one coroutine and I want to do second in the first one. I have a bug when I try to run second coroutine : "Suspension functions can be called only within coroutine body". I have everywhere coroutines so where is problem? Here is code:

 suspend fun firstCoroutine(): List<Decks> {

    val activeLanguage = userService.getActiveLanguage() ?: return emptyList()

    return suspendCancellableCoroutine { continuation ->

        db.collection("Decks")
            .whereArrayContains("languages", activeLanguage)
            .get()
            .addOnSuccessListener { documents ->

                val items = ArrayList<Decks>()

                if (documents != null) {
                    for (document in documents) {
                        val id: String = document.id

                        var knowCountForDeck: Int = secondCoroutine(id). <-- here is problem
                        
                        val name: String = document.data["name"] as String
                        val languages: List<String> =
                            document.data["languages"] as List<String>
                        items.add(Decks(id, name, languages))
                    }

                }
                continuation.resume(items)
            }
            .addOnFailureListener { err ->
                continuation.resumeWithException(err)
            }
    }
}

suspend fun secondCoroutine(collectionId: String): Int {

    val userId = userService.getCurrentUserId() ?: return 0

    return suspendCancellableCoroutine { continuation ->

        db.collection("Users").document(userId).collection(collectionId).get()
            .addOnSuccessListener { cards ->

                var knowCountForDeck = 0

                if (!cards.isEmpty) {
                    for (card in cards) {
                        if (card.data["status"] == "know") {
                            knowCountForDeck += 1
                        }
                    }
                }
                continuation.resume(knowCountForDeck)
            }
            .addOnFailureListener { err ->
                continuation.resumeWithException(err)
            }
    }
}

Solution

  • You are trying to call a coroutine from a callback, which is not part of your coroutine so it can't call suspend functions.

    One of the main advantages of coroutines is that you can avoid using callbacks and write your code sequentially.

    Many APIs already include suspend function alternatives to using callbacks. If I'm correct in guessing you're using Firebase, you can use the suspend function await() instead of using listeners. Then you don't need to use suspendCoroutine or suspendCancellableCoroutine to convert your callbacks into a suspend function:

    suspend fun firstCoroutine(): List<Decks> {
    
        val activeLanguage = userService.getActiveLanguage() ?: return emptyList()
    
        val documents = db.collection("Decks")
            .whereArrayContains("languages", activeLanguage)
            .get()
            .await()
        val items = documents.map { document ->
            val id: String = document.id
    
            var knowCountForDeck: Int = someSuspendFunction(id)
    
            val name: String = document.data["name"] as String
            val languages: List<String> =
                document.data["languages"] as List<String>
            Decks(id, name, languages)
        }
        return items
    }
    

    If you were working with an API that doesn't have a suspend function available, then to write your own with suspendCoroutine, I suggest writing a bare bones version that can work with any Task, and then use that within your specific application code. It would looks something like this:

    suspend fun <T> Task<T>.await() = suspendCoroutine<T> { continuation ->
        addOnSuccessListener {
            continuation.resume(it)
        }
        addOnFailureListener {
            continuation.resumeWithException(it)
        }
    }
    

    Or to follow Kotlin conventions better, you can return null instead of throwing an exception on recoverable failures:

    suspend fun <T: Any> Task<T>.awaitResultOrNull(): T? = suspendCoroutine<T> { continuation ->
        addOnSuccessListener {
            continuation.resume(it)
        }
        addOnFailureListener {
            continuation.resume(null)
        }
    }