question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Unable to access Repository's suspending function scope launched from ViewModel

See original GitHub issue

Overview

Expected

  • Launch a coroutine launch { ... } inside the Repository using the suspending method scope.
  • Suspending method is launched from a ViewModel usingviewModelScope.

Observed

  • When attempting to utilize the suspending function’s coroutine scope with withContext(Dispatchers.Default), the suspending function coroutine is not running.

Implementation

ViewModel

  1. The ViewModel uses viewModelScope to launch getContentList().
  2. getContentList() is a suspending function that calls the Repository with another suspending function getMainFeedList().
class ContentViewModel : ViewModel() {
    fun processEvent(...) {
        ...
        viewModelScope.launch {
            _feedViewState.value = getContentList(...)
        }
        ...
    }

   suspend private fun getContentList(...): LiveData<PagedList<Content>> =
       switchMap(getMainFeedList(isRealtime, timeframe)) { lce ->
           // Do something with result.
       }
}

Repository

  1. getMainFeedList() is a suspending function that uses withContext(Dispatchers.Default) in order to get the coroutine scope.
  2. getMainFeedList() returns LiveData with the result from a Firebase Firestore collection request, contentEnCollection.get().addOnCompleteListener.
  3. The Firestore result is saved to a Room DB with insertContentList(), from within the nested suspending coroutine launch { ... }. This suspending coroutine function is not working as the main feed of the app is empty on load.
object ContentRepository {
    suspend fun getMainFeedList(...): MutableLiveData<Lce<PagedListResult>> = withContext(Dispatchers.Default) {
        MutableLiveData<Lce<PagedListResult>>().also { lce ->
            val newContentList = arrayListOf<Content?>()
            contentEnCollection.get().addOnCompleteListener {
                arrayListOf<Content?>().also { contentList ->
                    it.result!!.documents.all { document ->
                        contentList.add(document.toObject(Content::class.java))
                        true
                    }
                    newContentList.addAll(contentList)
                }
                launch {
                    try {
                        database.contentDao().insertContentList(newContentList)
                    } catch (e: Exception) {
                        this.cancel()
                    }
                }.invokeOnCompletion { throwable ->
                    if (throwable == null)
                        lce.postValue(...)
                    else // Log Room error here.
                }
            }.addOnFailureListener {
                // Log Firestore error here.
                lce.postValue(...)
            }
        }
    }
}

Dao

@Dao
interface ContentDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertContentList(users: ArrayList<Content?>)
}

Attempted Solutions

Attempt #3 works as the main feed loads with data. However, manually launching a coroutine is not ideal.

  1. Initiating suspending coroutine scope outside of the asynchronous Firestore API call contentEnCollection.get() and using that scope inside the Firebase call.
object ContentRepository {
    suspend fun getMainFeedList(...): MutableLiveData<Lce<PagedListResult>> = withContext(Dispatchers.Default) {
        val scope: CoroutineScope = this
        MutableLiveData<Lce<PagedListResult>>().also { lce ->
            ...
                scope.launch { ... }
        }
}
  1. Launch coroutine with CoroutineScope() and use the suspending function’s coroutine scope context.
object ContentRepository {
    suspend fun getMainFeedList(...): MutableLiveData<Lce<PagedListResult>> = withContext(Dispatchers.Default) {
        val scope: CoroutineScope = this
        MutableLiveData<Lce<PagedListResult>>().also { lce ->
            ...
              CoroutineScope(scope.coroutineContext).launch { ... }
        }
}
  1. Launch new coroutine without the suspending function’s scope and manage cancelling the Job if there is an error.
CoroutineScope(Dispatchers.Default).launch {
    try {
        database.contentDao().insertContentList(newContentList)
    } catch (e: Exception) {
        this.cancel()
    }
}.invokeOnCompletion { throwable ->
    if (throwable == null)
        // Do more things.
    else
       // Log Room error.
}

Issue Analytics

  • State:open
  • Created 4 years ago
  • Comments:5 (2 by maintainers)

github_iconTop GitHub Comments

1reaction
zach-klippensteincommented, Oct 18, 2019

I’m not sure I understand exactly what the problem is – your insertContentList call is not being executed, or your MutableLiveData.postValue is never being executed?

I’ve got a couple questions about the rest of this code too:

  1. Why is your return type a MutableLiveData instead of LiveData? Do your callers also need to post their own values? If not, you can just use th liveData coroutine builder.
  2. Why are you adding values to contentList and then copying to newContentList, instead of just adding directly to newContentList?
  3. Why use invokeOnCompletion at all, you can just post the value or log errors from your launch body and catch body directly.
  4. This would be easier to read if you turned contentEnCollection into a suspend function or Flow first, so you can eliminate some callback nesting.
  5. It looks like getMainFeedList is only suspending to access the scope. In general, functions that return asynchronous types (like LiveData) should not be suspending, since they presumably don’t actually suspend the caller since their return value is already asynchronous. This function would be more idiomatic if you just passed the scope in as a parameter.
0reactions
AdamSHurwitzcommented, Nov 20, 2019

Extension function to remove callbacks

Thanks for sharing the suspendCancellableCoroutine pattern @zach-klippenstein! I used this to build a custom extension for Firebase’s realtime Firestore calls.

For Firebase’s Firestore database there are two types of calls.

  1. One time requests - addOnCompleteListener
  2. Realtime updates - addSnapshotListener

One time requests

For one time requests there is an await extension function provided by the library org.jetbrains.kotlinx:kotlinx-coroutines-play-services:X.X.X. The function returns results from addOnCompleteListener.

Resources

Realtime updates

The extension function awaitRealtime has checks including verifying the state of the continuation in order to see whether it is in isActive state. This is important because the function is called when the user’s main feed of content is updated either by a lifecycle event, refreshing the feed manually, or removing content from their feed. Without this check there will be a crash.

ExtenstionFuction.kt

data class QueryResponse(val packet: QuerySnapshot?, val error: FirebaseFirestoreException?)

suspend fun Query.awaitRealtime() = suspendCancellableCoroutine<QueryResponse> { continuation ->
    addSnapshotListener({ value, error ->
        if (error == null && continuation.isActive)
            continuation.resume(QueryResponse(value, null))
        else if (error != null && continuation.isActive)
            continuation.resume(QueryResponse(null, error))
    })
}

In order to handle errors the try/catch pattern is used.

Repository.kt

object ContentRepository {
    fun getMainFeedList(isRealtime: Boolean, timeframe: Timestamp) = flow<Lce<PagedListResult>> {
        emit(Loading())
        val labeledSet = HashSet<String>()
        val user = usersDocument.collection(getInstance().currentUser!!.uid)
        syncLabeledContent(user, timeframe, labeledSet, SAVE_COLLECTION, this)
        getLoggedInNonRealtimeContent(timeframe, labeledSet, this)        
    }
    // Realtime updates with 'awaitRealtime' used
    private suspend fun syncLabeledContent(user: CollectionReference, timeframe: Timestamp,
                                       labeledSet: HashSet<String>, collection: String,
                                       lce: FlowCollector<Lce<PagedListResult>>) {
        val response = user.document(COLLECTIONS_DOCUMENT)
            .collection(collection)
            .orderBy(TIMESTAMP, DESCENDING)
            .whereGreaterThanOrEqualTo(TIMESTAMP, timeframe)
            .awaitRealtime()
        if (response.error == null) {
            val contentList = response.packet?.documentChanges?.map { doc ->
                doc.document.toObject(Content::class.java).also { content ->
                    labeledSet.add(content.id)
                }
            }
            database.contentDao().insertContentList(contentList)
        } else lce.emit(Error(PagedListResult(null,
            "Error retrieving user save_collection: ${response.error?.localizedMessage}")))
    }
    // One time updates with 'await' used
    private suspend fun getLoggedInNonRealtimeContent(timeframe: Timestamp,
                                                      labeledSet: HashSet<String>,
                                                      lce: FlowCollector<Lce<PagedListResult>>) =
            try {
                database.contentDao().insertContentList(
                        contentEnCollection.orderBy(TIMESTAMP, DESCENDING)
                                .whereGreaterThanOrEqualTo(TIMESTAMP, timeframe).get().await()
                                .documentChanges
                                ?.map { change -> change.document.toObject(Content::class.java) }
                                ?.filter { content -> !labeledSet.contains(content.id) })
                lce.emit(Lce.Content(PagedListResult(queryMainContentList(timeframe), "")))
            } catch (error: FirebaseFirestoreException) {
                lce.emit(Error(PagedListResult(
                        null,
                        CONTENT_LOGGED_IN_NON_REALTIME_ERROR + "${error.localizedMessage}")))
            }
}

Style changes

I’ve refactored the recommended style changes.

  1. val lce = this
  2. Refactoring ArrayList extension function
  3. Refactoring all to map
val contentList = ArrayList<Content?>()
value!!.documentChanges.map { document ->
    document.document.toObject(Content::class.java).let { dismissedContent ->
        contentList.add(dismissedContent)
        labeledSet.add(dismissedContent.id)
    }
}
insertContentListToDb(scope, contentList)
Read more comments on GitHub >

github_iconTop Results From Across the Web

Scope confused in coroutines - android - Stack Overflow
The repository use coroutine to build the network call like this: suspend fun getUser() = GlobalScope { ... } The use case is...
Read more >
Jetpack Compose MVVM Project Example With Room and ...
Enrol to My 37 hour Advanced Android Development Course at Udemy (86% off, ...
Read more >
Kotlin Coroutines : View Model Scope example - YouTube
Get My Advanced Android Development Course at Udemy (Only $9.99) using this highly discounted link Become a professional, highly paid, ...
Read more >
Constructing a coroutine scope - Kt. Academy
This is where coroutines are generally started. In other layers, like in Use Cases or Repositories, we generally just use suspending functions.
Read more >
Best practices for coroutines in Android
Suspend functions in the ViewModel can be useful if instead of exposing state using a stream of data, only a single value needs...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found