I have a Repository defined as the following.
class StoryRepository {
private val firestore = Firebase.firestore
suspend fun fetchStories(): QuerySnapshot? {
return try {
firestore
.collection("stories")
.get()
.await()
} catch(e: Exception) {
Log.e("StoryRepository", "Error in fetching Firestore stories: $e")
null
}
}
}
I also have a ViewModel like this.
class HomeViewModel(
application: Application
) : AndroidViewModel(application) {
private var viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
private val storyRepository = StoryRepository()
private var _stories = MutableLiveData<List<Story>>()
val stories: LiveData<List<Story>>
get() = _stories
init {
uiScope.launch {
getStories()
}
uiScope.launch {
getMetadata()
}
}
private suspend fun getStories() {
withContext(Dispatchers.IO) {
val snapshots = storyRepository.fetchStories()
// Is this correct?
if (snapshots == null) {
cancel(CancellationException("Task is null; local DB not refreshed"))
return@withContext
}
val networkStories = snapshots.toObjects(NetworkStory::class.java)
val stories = NetworkStoryContainer(networkStories).asDomainModel()
_stories.postValue(stories)
}
}
suspend fun getMetadata() {
// Does some other fetching
}
override fun onCleared() {
super.onCleared()
viewModelJob.cancel()
}
}
As you can see, sometimes, StoryRepository().fetchStories()
may fail and return null
. If the return value is null
, I would like to not continue what follows after the checking for snapshots
being null
block. Therefore, I would like to cancel that particular coroutine (the one that runs getStories()
without cancelling the other coroutine (the one that runs getMetadata()
). How do I achieve this and is return
-ing from withContext
a bad-practice?
Although your approach is right, you can always make some improvements to make it simpler or more idiomatic (especially when you're not pleased with your own code).
These are just some suggestions that you may want to take into account:
You can make use of Kotlin Scope Functions, or more specifically the let
function like this:
private suspend fun getStories() = withContext(Dispatchers.IO) {
storyRepository.fetchStories()?.let { snapshots ->
val networkStories = snapshots.toObjects(NetworkStory::class.java)
NetworkStoryContainer(networkStories).asDomainModel()
} ?: throw CancellationException("Task is null; local DB not refreshed")
}
This way you'll be returning your data or throwing a CancellationException
if null
.
When you're working with coroutines inside a ViewModel you have a CoroutineScope ready to be used if you add this dependendy to your gradle file:
androidx.lifecycle:lifecycle-viewmodel-ktx:{version}
So you can use viewModelScope
to build your coroutines, which will run on the main thread:
init {
viewModelScope.launch {
_stories.value = getStories()
}
viewModelScope.launch {
getMetadata()
}
}
You can forget about cancelling its Job
during onCleared
since viewModelScope
is lifecycle-aware.
Now all you have left to do is handling the exception with a try-catch
block or with the invokeOnCompletion
function applied on the Job
returned by the launch
builder.