Handling Configuration Changes in Jetpack Compose

When developing Android applications with Jetpack Compose, handling configuration changes gracefully is essential for a seamless user experience. Configuration changes, such as screen rotations, language changes, or keyboard availability, can destroy and recreate the Activity, leading to potential data loss and disruptions in the UI. Jetpack Compose offers several mechanisms to handle these changes effectively.

Understanding Configuration Changes

Configuration changes occur when certain device settings change, prompting the Android system to recreate the Activity to adapt to the new configuration. Common configuration changes include:

  • Screen Rotation: Changing the device’s orientation between portrait and landscape.
  • Language Changes: Switching the system language.
  • Keyboard Availability: Opening or closing a hardware keyboard.
  • Density Changes: When the device’s screen density changes.

By default, Android destroys and recreates the Activity when a configuration change occurs. While this ensures that the app adapts to the new configuration, it can lead to:

  • Data Loss: Transient UI state is lost unless explicitly saved.
  • UI Reset: The UI is redrawn, causing potential flickering and interruptions.
  • Performance Overhead: Recreating the Activity takes time and resources.

Approaches to Handle Configuration Changes in Jetpack Compose

Jetpack Compose provides several techniques to manage configuration changes efficiently:

1. Using remember and rememberSaveable

The remember composable is used to retain state across recompositions, while rememberSaveable is designed to preserve state across configuration changes and process death.

Step 1: Add Dependency

Ensure that you have included necessary dependencies in your build.gradle file.


dependencies {
    implementation("androidx.compose.runtime:runtime:1.6.0")
    implementation("androidx.compose.runtime:runtime-saveable:1.6.0") // Or newer
}
Step 2: Using rememberSaveable to Persist UI State

import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

@Composable
fun CounterScreen() {
    var count by rememberSaveable { mutableStateOf(0) }

    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Count: $count")
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { count++ }) {
            Text(text = "Increment")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun CounterScreenPreview() {
    CounterScreen()
}

In this example:

  • rememberSaveable ensures that the count state is preserved even when the Activity is recreated due to a configuration change (like screen rotation).

2. Using ViewModel

The ViewModel class is designed to store and manage UI-related data in a lifecycle-conscious way. It is often used with Compose to survive configuration changes and prevent data loss.

Step 1: Add ViewModel Dependency

Ensure you have the necessary dependency for ViewModel in your build.gradle file:


dependencies {
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1")
}
Step 2: Implement ViewModel to Retain State

import androidx.compose.runtime.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel

class CounterViewModel : ViewModel() {
    private val _count = mutableStateOf(0)
    val count: State = _count

    fun increment() {
        _count.value++
    }
}

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Count: ${viewModel.count.value}")
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = { viewModel.increment() }) {
            Text(text = "Increment")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun CounterScreenPreview() {
    CounterScreen()
}

In this example:

  • CounterViewModel holds the count state and survives configuration changes because ViewModel instances are retained during such events.
  • viewModel() is used to retrieve or create the CounterViewModel scoped to the lifecycle of the composable.

3. Using Accompanist System UI Controller

To manage system UI elements (like status bar color) during configuration changes, use Accompanist’s System UI Controller.

Step 1: Add Accompanist Dependency

Include the necessary Accompanist dependency in your build.gradle file:


dependencies {
    implementation("com.google.accompanist:accompanist-systemuicontroller:0.34.0")
}
Step 2: Implementing System UI Changes

import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import androidx.compose.ui.tooling.preview.Preview

@Composable
fun SystemUIControllerExample() {
    val systemUiController = rememberSystemUiController()

    // Set status bar color
    systemUiController.setStatusBarColor(
        color = Color.Red
    )

    // Set navigation bar color
    systemUiController.setNavigationBarColor(
        color = Color.Black
    )

    Surface(color = Color.White) {
        // Content goes here
    }
}

@Preview(showBackground = true)
@Composable
fun SystemUIControllerExamplePreview() {
    SystemUIControllerExample()
}

In this example:

  • rememberSystemUiController is used to access and modify the system UI settings.
  • setStatusBarColor and setNavigationBarColor functions allow you to change the colors of the system UI elements.
  • The changes will be automatically reapplied after configuration changes, ensuring consistency.

4. Preventing Activity Recreation (Not Recommended)

While it’s generally best to handle configuration changes gracefully by preserving state, you can prevent the Activity from being recreated by handling the configuration change manually in the AndroidManifest.xml. This approach is not recommended because it requires you to manage the configuration changes yourself, which can become complex.


<activity
    android:name=".MainActivity"
    android:configChanges="orientation|screenSize"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

If you use this approach, you must override the onConfigurationChanged method in your Activity to handle the changes.


import android.content.res.Configuration
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)

        // Handle configuration changes here
        if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            // Handle landscape orientation
        } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
            // Handle portrait orientation
        }
    }
}

Note: This method is generally discouraged as it can lead to more complex and error-prone code.

Conclusion

Handling configuration changes in Jetpack Compose is crucial for creating robust and user-friendly Android applications. By using rememberSaveable to preserve simple UI states and ViewModel for managing more complex data, you can ensure that your app behaves consistently across various configurations. Libraries like Accompanist’s System UI Controller further simplify managing system UI elements during these changes. Embracing these techniques will help you build a seamless and reliable user experience.