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:This way you'll be returning your data or throwing a
CancellationException
ifnull
.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:
So you can use
viewModelScope
to build your coroutines, which will run on the main thread:You can forget about cancelling its
Job
duringonCleared
sinceviewModelScope
is lifecycle-aware.Now all you have left to do is handling the exception with a
try-catch
block or with theinvokeOnCompletion
function applied on theJob
returned by thelaunch
builder.