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
  • viewModelScope cancelled but operation result still needed

Step-by-Step Fix

  1. 1.**Use shared in-flight operation across ViewModel instances":
  2. 2.```kotlin
  3. 3.class MyViewModel(
  4. 4.private val repository: DataRepository
  5. 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. 1.**Use SavedStateHandle to preserve operation state":
  2. 2.```kotlin
  3. 3.class MyViewModel(
  4. 4.private val savedStateHandle: SavedStateHandle,
  5. 5.private val repository: DataRepository
  6. 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. 1.**Use WorkManager for operations that must survive process death":
  2. 2.```kotlin
  3. 3.// For operations that must continue even if the app is killed,
  4. 4.// use WorkManager instead of viewModelScope
  5. 5.class DataSyncWorker(
  6. 6.context: Context,
  7. 7.params: WorkerParameters
  8. 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 SavedStateHandle to 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 StateFlow to share data between ViewModel and UI