Handling Configuration Changes in XML UI

In Android development, handling configuration changes is a crucial aspect of creating robust and user-friendly applications. Configuration changes, such as screen rotation, keyboard availability, or locale changes, can lead to the Activity being destroyed and recreated. If not handled properly, this can result in data loss and a poor user experience. This article focuses on effectively managing configuration changes in XML-based UI environments.

Understanding Configuration Changes

Configuration changes occur when the device undergoes a change in its setup, such as screen orientation, keyboard availability, locale, or even multi-window mode. By default, Android handles most configuration changes by restarting the Activity. While this ensures that the app adapts to the new configuration, it can also lead to data loss and performance issues if the Activity is recreated without proper handling.

Why Proper Handling is Essential

  • Data Preservation: Prevents loss of UI state data like input values or displayed data.
  • User Experience: Maintains a smooth and uninterrupted experience during configuration changes.
  • Performance: Avoids unnecessary resource consumption by preventing the Activity from being fully recreated when not necessary.

Basic Approach: Saving and Restoring State

The most basic approach to handle configuration changes is to save the Activity’s state before it is destroyed and restore it when the Activity is recreated. Android provides two primary mechanisms for this:

  1. onSaveInstanceState() method
  2. ViewModel and related lifecycle components (discussed later)

Using onSaveInstanceState()

The onSaveInstanceState() method allows you to save key data that can be used to restore the Activity’s state.

Step 1: Override onSaveInstanceState()

Override the onSaveInstanceState() method in your Activity:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putString("my_text", myTextView.getText().toString());
    outState.putInt("my_integer", myIntegerValue);
}
Step 2: Restore the State in onCreate()

In the onCreate() method, restore the saved state from the savedInstanceState Bundle:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    myTextView = findViewById(R.id.my_text_view);

    if (savedInstanceState != null) {
        String savedText = savedInstanceState.getString("my_text");
        int savedInteger = savedInstanceState.getInt("my_integer");

        myTextView.setText(savedText);
        myIntegerValue = savedInteger;
    } else {
        // Initial setup if no state was saved
        myTextView.setText("Hello, World!");
        myIntegerValue = 0;
    }
}

Example Scenario

Imagine you have an EditText field where the user enters some text. Without handling configuration changes, rotating the screen would cause the text to disappear.

<EditText
    android:id="@+id/my_edit_text"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="Enter text here"/>

Here’s how you could preserve the text using onSaveInstanceState():

private EditText myEditText;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    myEditText = findViewById(R.id.my_edit_text);

    if (savedInstanceState != null) {
        String savedText = savedInstanceState.getString("edit_text_value");
        myEditText.setText(savedText);
    }
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putString("edit_text_value", myEditText.getText().toString());
}

Advanced Approach: Using ViewModel

For more complex data, managing state directly in the Activity can become cumbersome. Android’s ViewModel, as part of the Architecture Components, provides a lifecycle-aware way to manage UI-related data.

Adding Dependencies

Include the necessary dependencies in your build.gradle file:

dependencies {
    implementation "androidx.lifecycle:lifecycle-viewmodel:2.6.1"
    implementation "androidx.lifecycle:lifecycle-livedata:2.6.1"
    annotationProcessor "androidx.lifecycle:lifecycle-compiler:2.6.1"
}

Creating a ViewModel Class

Define a ViewModel class that holds the UI-related data:

import androidx.lifecycle.ViewModel;

public class MyViewModel extends ViewModel {
    private String myText;

    public String getMyText() {
        return myText;
    }

    public void setMyText(String myText) {
        this.myText = myText;
    }
}

Using ViewModel in Activity

In your Activity, access and use the ViewModel:

import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.widget.EditText;

public class MainActivity extends AppCompatActivity {

    private MyViewModel viewModel;
    private EditText myEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        myEditText = findViewById(R.id.my_edit_text);
        viewModel = new ViewModelProvider(this).get(MyViewModel.class);

        if (viewModel.getMyText() != null) {
            myEditText.setText(viewModel.getMyText());
        }

        myEditText.setOnTextChangedListener(new TextWatcher() {
           @Override
           public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

           @Override
           public void onTextChanged(CharSequence s, int start, int before, int count) {
               viewModel.setMyText(s.toString());
           }

           @Override
           public void afterTextChanged(Editable s) {}
        });
    }
}

Using a ViewModel ensures that the data persists even when the Activity is destroyed and recreated during configuration changes.

Handling Configuration Changes Manually

Sometimes, you might want to prevent the Activity from being recreated altogether. In such cases, you can handle specific configuration changes manually.

Step 1: Declare the Change in the Manifest

In your AndroidManifest.xml, declare the configuration changes you want to handle by adding the android:configChanges attribute to the <activity> tag.

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

This example declares that the Activity will handle orientation and screen size changes itself.

Step 2: Override onConfigurationChanged()

Override the onConfigurationChanged() method in your Activity to handle the changes.

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

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        // Check the orientation of the screen
        if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
            Toast.makeText(this, "landscape", Toast.LENGTH_SHORT).show();
        } else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT){
            Toast.makeText(this, "portrait", Toast.LENGTH_SHORT).show();
        }
    }
}

When a declared configuration changes, onConfigurationChanged() is called instead of restarting the Activity.

Best Practices for Handling Configuration Changes

  • Use ViewModel for UI-related Data: For complex data that needs to survive configuration changes, use ViewModel.
  • onSaveInstanceState() for Simple UI State: Use onSaveInstanceState() for saving simple UI states like EditText values.
  • Manual Handling Judiciously: Only handle configuration changes manually if necessary and if you can efficiently manage all the related logic.
  • Avoid Complex Logic in onConfigurationChanged(): Keep the onConfigurationChanged() method light to avoid performance bottlenecks.
  • Test Thoroughly: Always test your application with different configuration changes to ensure data is preserved and the UI behaves as expected.

Conclusion

Effectively handling configuration changes in Android is crucial for providing a seamless user experience and preventing data loss. Using techniques like onSaveInstanceState(), ViewModel, and manual handling with onConfigurationChanged(), you can build robust applications that gracefully adapt to various device configurations. Properly implemented, these strategies contribute significantly to the overall quality and usability of your Android app. Consider carefully whether it’s appropriate to prevent restarts due to configuration changes since these restarts are often necessary to load appropriate resources for the new configuration.