Introduction
While viewModelScope is automatically cancelled when the ViewModel is cleared, long-running operations that outlive the ViewModel's logical lifecycle (such as screen rotation) can cause issues. The old ViewModel is destroyed but its coroutines continue running until completion. When a new ViewModel is created after rotation, it starts the same operation again, leading to duplicate work, wasted resources, and potential race conditions between the old and new coroutines.
Symptoms
- Same API call executed twice after screen rotation
- Memory increases after each configuration change
- Old coroutine completes and tries to update destroyed UI
- Race condition between old and new ViewModel coroutines
- Database write conflicts from duplicate operations
Debug ViewModel lifecycle: ```kotlin class MyViewModel : ViewModel() { init { Log.d("ViewModel", "Created: ${this.hashCode()}") }
override fun onCleared() { super.onCleared() Log.d("ViewModel", "Cleared: ${this.hashCode()}") // Coroutines in viewModelScope are cancelled here } } ```
Common Causes
- ViewModel initiates long operation on every creation
- Operation not idempotent, running twice causes data issues
- Result from old ViewModel delivered after new one already started
- No mechanism to share in-flight operations across ViewModel instances
viewModelScopecancelled but operation result still needed
Step-by-Step Fix
- 1.**Use shared in-flight operation across ViewModel instances":
- 2.```kotlin
- 3.class MyViewModel(
- 4.private val repository: DataRepository
- 5.) : ViewModel() {
private val _data = MutableStateFlow<DataState>(DataState.Loading) val data = _data.asStateFlow()
// Shared operation survives ViewModel recreation private var loadDataJob: Job? = null
fun loadData() { // Don't start if already loading if (_data.value is DataState.Loading) return
loadDataJob?.cancel() // Cancel any previous attempt
loadDataJob = viewModelScope.launch { _data.value = DataState.Loading try { val result = repository.fetchData() _data.value = DataState.Success(result) } catch (e: Exception) { _data.value = DataState.Error(e) } } } } ```
- 1.**Use SavedStateHandle to preserve operation state":
- 2.```kotlin
- 3.class MyViewModel(
- 4.private val savedStateHandle: SavedStateHandle,
- 5.private val repository: DataRepository
- 6.) : ViewModel() {
private val _data = MutableStateFlow<DataState>( savedStateHandle.get<DataState>("data") ?: DataState.Loading ) val data = _data.asStateFlow()
init { // Restore state after rotation if (_data.value !is DataState.Loading && _data.value !is DataState.Success) { viewModelScope.launch { _data.value = DataState.Loading _data.value = try { DataState.Success(repository.fetchData()) } catch (e: Exception) { DataState.Error(e) } } }
// Save state for next instance viewModelScope.launch { data.collect { savedStateHandle["data"] = it } } } } ```
- 1.**Use WorkManager for operations that must survive process death":
- 2.```kotlin
- 3.// For operations that must continue even if the app is killed,
- 4.// use WorkManager instead of viewModelScope
- 5.class DataSyncWorker(
- 6.context: Context,
- 7.params: WorkerParameters
- 8.) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result { return try { repository.syncData() Result.success() } catch (e: Exception) { if (runAttemptCount < 3) { Result.retry() } else { Result.failure() } } } }
// In ViewModel - trigger work that survives everything fun startSync() { val workRequest = OneTimeWorkRequestBuilder<DataSyncWorker>() .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS) .build()
WorkManager.getInstance(application).enqueueUniqueWork( "data_sync", ExistingWorkPolicy.KEEP, // Don't duplicate workRequest ) } ```
Prevention
- Check if operation is already in progress before starting
- Use
SavedStateHandleto preserve state across configuration changes - Use WorkManager for operations that must survive process death
- Make API calls idempotent so duplicates do not cause issues
- Cancel old operations when new ViewModel is created
- Use
StateFlowto share data between ViewModel and UI