Unable to access Repository's suspending function scope launched from ViewModel
See original GitHub issueOverview
Expected
- Launch a coroutine
launch { ... }
inside the Repository using the suspending method scope. - Suspending method is launched from a ViewModel using
viewModelScope
.
Observed
- When attempting to utilize the suspending function’s coroutine scope with
withContext(Dispatchers.Default)
, the suspending function coroutine is not running.
Implementation
ViewModel
- The ViewModel uses
viewModelScope
to launchgetContentList()
. getContentList()
is a suspending function that calls the Repository with another suspending functiongetMainFeedList()
.
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
getMainFeedList()
is a suspending function that useswithContext(Dispatchers.Default)
in order to get the coroutine scope.getMainFeedList()
returns LiveData with the result from a Firebase Firestore collection request,contentEnCollection.get().addOnCompleteListener.
- The Firestore result is saved to a Room DB with
insertContentList()
, from within the nested suspending coroutinelaunch { ... }
. 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.
- 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 { ... }
}
}
- 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 { ... }
}
}
- 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:
- Created 4 years ago
- Comments:5 (2 by maintainers)
Top 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 >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
I’m not sure I understand exactly what the problem is – your
insertContentList
call is not being executed, or yourMutableLiveData.postValue
is never being executed?I’ve got a couple questions about the rest of this code too:
MutableLiveData
instead ofLiveData
? Do your callers also need to post their own values? If not, you can just use thliveData
coroutine builder.contentList
and then copying tonewContentList
, instead of just adding directly tonewContentList
?invokeOnCompletion
at all, you can just post the value or log errors from yourlaunch
body andcatch
body directly.contentEnCollection
into a suspend function or Flow first, so you can eliminate some callback nesting.getMainFeedList
is only suspending to access the scope. In general, functions that return asynchronous types (likeLiveData
) 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.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.
addOnCompleteListener
addSnapshotListener
One time requests
For one time requests there is an
await
extension function provided by the libraryorg.jetbrains.kotlinx:kotlinx-coroutines-play-services:X.X.X
. The function returns results fromaddOnCompleteListener
.Resources
Realtime updates
The extension function
awaitRealtime
has checks including verifying the state of thecontinuation
in order to see whether it is inisActive
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
In order to handle errors the
try
/catch
pattern is used.Repository.kt
Style changes
I’ve refactored the recommended style changes.
val lce = this
ArrayList
extension functionall
tomap