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
CancellationExceptionlogged 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.launchfor data loading that should survive rotation - Not using ViewModel for long-running operations
repeatOnLifecyclerestarts collection on every lifecycle change- State not saved and restored during configuration change
- Mixing
lifecycleScopeandviewModelScopeincorrectly
Step-by-Step Fix
- 1.Use viewModelScope for data that should survive rotation:
- 2.```kotlin
- 3.class MyViewModel(application: Application) : AndroidViewModel(application) {
- 4.private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
- 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.Use SavedStateHandle for small state preservation:
- 2.```kotlin
- 3.class MyViewModel(
- 4.private val savedStateHandle: SavedStateHandle
- 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.Use stateIn with proper sharing for hot flows:
- 2.```kotlin
- 3.class MyViewModel(repository: Repository) : ViewModel() {
- 4.val items: StateFlow<List<Item>> = repository.getItems()
- 5..stateIn(
- 6.scope = viewModelScope,
- 7.started = SharingStarted.WhileSubscribed(5000), // 5 second buffer
- 8.initialValue = emptyList()
- 9.)
- 10.// SharingStarted.WhileSubscribed(5000) keeps the upstream active
- 11.// for 5 seconds after the last collector disconnects (rotation gap)
- 12.}
- 13.
` - 14.Distinguish UI coroutines from data coroutines:
- 15.```kotlin
- 16.class MainActivity : AppCompatActivity() {
- 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
viewModelScopefor data operations,lifecycleScopefor UI operations - Use
StateFlowwithstateInto cache data across configuration changes - Use
SavedStateHandlefor 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:configChangesfor simple cases where recreation is not needed