Introduction

When an Android device rotates, the Activity is destroyed and recreated. Coroutines launched with lifecycleScope are cancelled along with the old Activity, potentially losing in-flight work. This is by design for UI-related coroutines but problematic for data loading, form submissions, and other operations that should survive configuration changes.

Symptoms

  • Network request restarts after screen rotation
  • Loading spinner shows again after rotation
  • User form input is lost during long operation
  • CancellationException logged on rotation
  • Data loading starts over after configuration change

Example scenario: ```kotlin class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

// This coroutine is cancelled on rotation! lifecycleScope.launch { val data = withContext(Dispatchers.IO) { repository.fetchLargeDataset() // Takes 10 seconds } // If user rotates at 5 seconds, this is never reached updateUI(data) } } } ```

Common Causes

  • Using lifecycleScope.launch for data loading that should survive rotation
  • Not using ViewModel for long-running operations
  • repeatOnLifecycle restarts collection on every lifecycle change
  • State not saved and restored during configuration change
  • Mixing lifecycleScope and viewModelScope incorrectly

Step-by-Step Fix

  1. 1.Use viewModelScope for data that should survive rotation:
  2. 2.```kotlin
  3. 3.class MyViewModel(application: Application) : AndroidViewModel(application) {
  4. 4.private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
  5. 5.val uiState: StateFlow<UiState> = _uiState

init { // viewModelScope survives configuration changes viewModelScope.launch { try { val data = repository.fetchLargeDataset() _uiState.value = UiState.Success(data) } catch (e: Exception) { _uiState.value = UiState.Error(e.message) } } } }

// In Activity: collect the state (UI updates restart, but data does not re-fetch) class MainActivity : AppCompatActivity() { private val viewModel: MyViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> updateUI(state) } } } } } ```

  1. 1.Use SavedStateHandle for small state preservation:
  2. 2.```kotlin
  3. 3.class MyViewModel(
  4. 4.private val savedStateHandle: SavedStateHandle
  5. 5.) : ViewModel() {

companion object { private const val KEY_SELECTED_ID = "selected_id" }

var selectedId: String get() = savedStateHandle[KEY_SELECTED_ID] ?: "" set(value) { savedStateHandle[KEY_SELECTED_ID] = value }

// State survives process death too, not just rotation } ```

  1. 1.Use stateIn with proper sharing for hot flows:
  2. 2.```kotlin
  3. 3.class MyViewModel(repository: Repository) : ViewModel() {
  4. 4.val items: StateFlow<List<Item>> = repository.getItems()
  5. 5..stateIn(
  6. 6.scope = viewModelScope,
  7. 7.started = SharingStarted.WhileSubscribed(5000), // 5 second buffer
  8. 8.initialValue = emptyList()
  9. 9.)
  10. 10.// SharingStarted.WhileSubscribed(5000) keeps the upstream active
  11. 11.// for 5 seconds after the last collector disconnects (rotation gap)
  12. 12.}
  13. 13.`
  14. 14.Distinguish UI coroutines from data coroutines:
  15. 15.```kotlin
  16. 16.class MainActivity : AppCompatActivity() {
  17. 17.private val viewModel: MyViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

// UI coroutine: OK to restart on rotation lifecycleScope.launch { animateEntryTransition() }

// Data is in ViewModel: survives rotation lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { state -> render(state) } } } } } ```

Prevention

  • Use viewModelScope for data operations, lifecycleScope for UI operations
  • Use StateFlow with stateIn to cache data across configuration changes
  • Use SavedStateHandle for critical user state that must survive process death
  • Use repeatOnLifecycle(Lifecycle.State.STARTED) for UI collection
  • Test rotation during active operations in your QA process
  • Consider android:configChanges for simple cases where recreation is not needed