Search code examples
androidkotlinkotlin-coroutinesandroid-viewmodel

Can I start a coroutine in `ViewModel.onCleared()`?


I want the data to be saved to a file when the viewmodel is destroyed, here is the code:

@HiltViewModel
class MyViewModel @Inject constructor(@ApplicationContext private val context: Context) :
  ViewModel() {

  private val _uiState = MutableStateFlow("jack")
  val uiState: StateFlow<String> = _uiState.asStateFlow()
  @OptIn(ExperimentalSerializationApi::class)
  override fun onCleared() {
    super.onCleared()
    // Writing data to a file
    CoroutineScope(IO).launch {
      val file = File(context.filesDir, "name.json")
      val outputStream = file.outputStream()
      outputStream.use {
        _uiState.collect { value ->
          Json.encodeToStream(value, outputStream)
        }
      }
    }
  }
}
  1. By doing this, I lose the reference to the job object, does this follow structured concurrency?
  2. Should the code for storing data come before or after the parent method super.onCleared()?
  3. Will this code run successfully?
  4. If I start a coroutine using viewModelScope.launch(), will the coroutine run successfully?

Solution

    1. In this case it's fine, since you don't have any condition under which you would want to cancel this coroutine. But since using CoroutineScope(IO).launch is a common anti-pattern, I suggest using GlobalScope.launch(IO) with @OptIn(DelicateCoroutinesApi::class) to indicate you are intentionally doing it.
    2. Doesn't matter.
    3. Yes, it will write the file unless there's an IO exception. However, you are leaking the StateFlow and the OutputStream forever because collect called on a SharedFlow or StateFlow never returns.
    4. Most likely not, but it's a race condition. encodeToStream is not interruptible. If that line of code is reached before the viewModelScope is cancelled, then it will succeed in writing, but if it doesn't, then it won't.

    You should change your code as follows to prevent the leak:

      @OptIn(ExperimentalSerializationApi::class)
      @OptIn(DelicateCoroutinesApi::class)
      override fun onCleared() {
        super.onCleared()
        // Writing data to a file
        GlobalScope.launch(IO) {
          val file = File(context.filesDir, "name.json")
          try {
            file.outputStream().use { outputStream ->
              Json.encodeToStream(uiState.value, outputStream)
            }
          } catch(e: IOException) {
            Log.e(TAG, "Failed to write ui state", e)
          }
        }
      }