Home > Blockchain >  Run coroutine past viewModelScope lifecycle
Run coroutine past viewModelScope lifecycle

Time:11-07

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:

  1. Inserts the item to the local database
  2. 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)
         }
     }
}
  • Related