I need to make sure a suspending function will be completed even if the viewModel
is cleared.
For example, I have a button in my app that when clicked calls viewModel.addItem(item)
, that function then calls the repository which does two things:
- Inserts the item to the local database
- Sends a request to an API to insert the item as well (and waits for the response to update the item).
I want to finish the activity after step 1, BUT still continue with step 2 even after the activity finishes (which also clears the viewModel
of course, which will cancel the API request).
I want it to behave like this because if I'll just wait for both to complete and the user have a slow internet connection, it will probably timeout and make the user wait on this activity. Instead I feel like its a much better user experience to complete the task locally (finish the activity and let him do other stuff in the app) and wait for the server's response to then sync what ever happened (of course if the task didn't complete at all, I'll have to sync it when the user will have a better internet connection, but that's off topic).
Currently I have something like this (which describes the bad scenario with the timeout):
Activity:
viewModel.addItemLiveData.observe(this) { success ->
if(success) //Toast well done
else //Something went wrong
finish()
}
button.setOnClickListener {
val item = ... //Collected from the UI
viewModel.addItem(item)
}
ViewModel:
private val _addItemLiveData = MutableLiveData<Boolean>()
val addItemLiveData: LiveData<Boolean> = _addItemLiveData
fun addItem(item: Item) = viewModelScope.launch {
val id = repository.addItem(item)
_addItemLiveData.value = id > 0
}
Repository:
suspend fun addItem(item: Item) {
val id = roomDao.insert(item)
//This part needs to run after the viewModel's lifecycle
val res = apiService.insert(item) //Wrapped in a convenient class to indicate Success or Error
if(res is Resource.Success) {
roomDao.update(res.value)
}
}
(Some code is omitted for brevity)
I thought maybe I could perform a GlobalScope.launch
somehow, but I read its not best practice to use it (and I'm still quite new to kotlin so I'm not even entirely sure how to do it).
Also, maybe I'm not approaching this problem correctly. Any help is appreciated.
CodePudding user response:
You can create a coroutine scope tied to your application lifecycle. If you are using dependency injection, you should inject this scope to your viewModel. If not, you can create a scope in your custom application class.
class MyCustomApplication: Application() {
val applicationScope = CoroutineScope(SupervisorJob())
}
class MyViewModel(private val app: MyCustomApplication) { // provide an instance of your custom application to view model
suspend fun addItem(item: Item) {
val id = roomDao.insert(item)
app.applicationScope.launch {
val res = apiService.insert(item) //Wrapped in a convenient class to indicate Success or Error
if(res is Resource.Success) {
roomDao.update(res.value)
}
}
}
}
You can go through this article for more details.
CodePudding user response:
You can use NonCancellable
context to create a suspending block that won't be interrupted if job or scope gets canceled:
suspend fun addItem(item: Item) {
val id = roomDao.insert(item)
//This part needs to run after the viewModel's lifecycle
withContext(NonCancellable) {
val res = apiService.insert(item) //Wrapped in a convenient class to indicate Success or Error
if(res is Resource.Success) {
roomDao.update(res.value)
}
}
}