How can I get runBlocking in Kotlin to work correctly?

48 Views Asked by At

My goal here is to have loadInformationFromFirestore, which reads data from a Firestore database, complete before the code after it (here just a Timber logging message) is executed. When I run getGroupSummaries, these are the logging messages I see:

After loadUserInformationFromFirestore in runBlocking

After loadUserInformationFromFirestore after runBlocking

In LoadUserInformationFromFirestore

This says to me that "runBlocking" is not blocking at all -- the runBlocking returns before LoadUserInformationFromFirestore completes. Any advice would be appreciated.

var mFirestore: FirebaseFirestore? = null

fun getGroupSummaries(groupInfo: GroupInformation, adminUserInfo: UserInformation) {
    mFirestore = FirebaseFirestore.getInstance()

    val uid = "3iHWCsGmKvfq4w9OgDNWZsSs8wy2"

    runBlocking {
        val loadUserInformationJob = viewModelScope.launch { loadUserInformationFromFirestore(uid) }
        loadUserInformationJob.join()
        Timber.d("After loadUserInformationFromFirestore in runBlocking")
    }
    Timber.d("After loadUserInformationFromFirestore after runBlocking")
}

suspend fun loadUserInformationFromFirestore(uid: String) {
    try {
        // Get document from Firestore
        val docRef =
            mFirestore!!.collection("Users").document(uid).collection("UserProfile").document(uid)

        docRef.get().addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val document = task.result
                if (document.exists()) {
                    Timber.d("In LoadUserInformationFromFirestore")
                } else if (!document.exists()) {
                    Timber.e(
                        "In LoadInformationFromFirestore, User document does not exist. Send user through into questions."
                    )
                } else {
                    Timber.e("In LoadInformationFromFirestore, Error loading user document")
                }
            } else {
                Timber.e(
                    "In LoadInformationFromFirestore, get Firestore failed with ",
                    task.exception
                )
            }
        }
    } catch (e: Exception) {
        Timber.w("Error getting Firebase User", e)
    }
}
1

There are 1 best solutions below

1
Rakib Hasan On

I think the issue is related to the asynchronous nature of Firestore callbacks. The addOnCompleteListener callback inside loadUserInformationFromFirestore is asynchronous, and runBlocking doesn't wait for it to complete.

You can use suspendCoroutine or suspendCancellableCoroutine to convert the asynchronous Firestore task into a suspendable coroutine. This way, you can ensure that the runBlocking coroutine will wait for the Firestore task to complete before moving on.

See here:

var mFirestore: FirebaseFirestore? = null

fun getGroupSummaries(groupInfo: GroupInformation, adminUserInfo: UserInformation) {
    mFirestore = FirebaseFirestore.getInstance()

    val uid = "3iHWCsGmKvfq4w9OgDNWZsSs8wy2"

    runBlocking {
        try {
            val userInformation = loadUserInformationFromFirestore(uid)
            Timber.d("After loadUserInformationFromFirestore in runBlocking")
        } catch (e: Exception) {
            Timber.e("Error loading user information", e)
        }
    }
    Timber.d("After loadUserInformationFromFirestore after runBlocking")
}

suspend fun loadUserInformationFromFirestore(uid: String): UserInformation = suspendCancellableCoroutine { continuation ->
    try {
        // Get document from Firestore
        val docRef =
            mFirestore!!.collection("Users").document(uid).collection("UserProfile").document(uid)

        docRef.get().addOnCompleteListener { task ->
            if (task.isSuccessful) {
                val document = task.result
                if (document.exists()) {
                    Timber.d("In LoadUserInformationFromFirestore")
                    // Assuming UserInformation is a class you want to return
                    continuation.resume(UserInformation(/* populate with data */))
                } else {
                    Timber.e("In LoadInformationFromFirestore, User document does not exist.")
                    continuation.resumeWithException(DocumentNotFoundException("User document not found"))
                }
            } else {
                Timber.e("In LoadInformationFromFirestore, get Firestore failed with ", task.exception)
                continuation.resumeWithException(task.exception ?: RuntimeException("Unknown error"))
            }
        }
    } catch (e: Exception) {
        Timber.w("Error getting Firebase User", e)
        continuation.resumeWithException(e)
    }
}

class DocumentNotFoundException(message: String) : RuntimeException(message)

Here, the runBlocking coroutine will wait for the Firestore task to complete before moving on to the next line. Additionally, it handles exceptions and provides a more structured way of returning the result.

Hope it helps!