Search code examples
androidkotlinviewmodelkotlin-coroutinesjobs

How to know when job from viewModel is done


I am trying to figure out how jobs with coroutines work. Basically, I want to launch this coroutine from FirstFragment and after that navigate to SecondFragment and get notified when this job is done. I call getData() in FirstFragment onViewCreated() and navigate to SecondFragment. Whether I write getData().isCompleted or getData().invokeOnCompletion { } in SecondFragment nothing happens. I don't know if I am missing something or not starting job correctly or something else.

private val _data = MutableStateFlow<GetResource<String>?>(null)
val data: StateFlow<GetResource<String>?> = _data

fun getData() = viewModelScope.launch {
    repository.getData().collect {
        _data.value = it
    }
}

Solution

  • A Flow from a database never completes because it is supposed to monitor the database for changes indefinitely. It only stops when the coroutine is cancelled. Therefore the Job that collects such a Flow will never complete. Also, if you call getData() on the repo again, you are getting a new Flow instance each time.

    Regardless of what you're doing, you need to be sure you are using the same ViewModel instance between both fragments by scoping it to the Activity. (Use by activityViewModels() for example.) This is so the viewModelScope won't be cancelled during the transition between Fragments.

    If all you need is a single item from the repo one time, probably the simplest thing to do would be to expose a suspend function from the repo instead of a Flow. Then turn it into a Deferred. Maybe by making it a Lazy, you can selectively decide when to start retrieving the value. Omit the lazy if you just want to start retrieving the value immediately when the first Fragment starts.

    // In the shared view model:
    val data: Deferred<GetResource<String>> by lazy { 
        viewModelScope.async {
          repository.getData() // suspend function returning GetResource<String>
        }
      }
    
    fun startDataRetrieval() { data } // access the lazy property to start its coroutine
    
    // In second fragment:
    lifecycleScope.launch {
      val value = mySharedViewModel.data.await()
      // do something with value
    }
    

    But if you have to have the Flow because you’re using it for other purposes:

    If you just want the first available value from the Flow, have the second Fragment monitor your data StateFlow for its first valid value.

    lifecycleScope.launch {
      val value = mySharedViewModel.data.filterNotNull().first()
      // do something with first arrived value
    }
    

    And you can use SharedFlow so you don’t have to make the data type nullable. If you do this you can omit filterNotNull() above. In your ViewModel, it’s easier to do this with shareIn than your code that has to use a backing property and manually collect the source.

    val data: SharedFlow<GetResource<String>> = repository.getData()
      .shareIn(viewModelScope, replay = 1, SharingStarted.Eagerly)
    

    If you need to wait before starting the collection to the SharedFlow, then you could make the property lazy.