Logo Icon Logo
A Crowd-sourced Cookbook on Writing Great Android® Apps
GitHub logo Twitter logo OReilly Book Cover Art

Handling Configuration Changes by Decoupling the View from the Model

Author: Alex Leffelman
Published? true
FormatLanguage: WikiFormat

Problem:

When your device's configuration changes (most frequently due to an orientation change), your Activity is destroyed and recreated, making state information difficult to maintain.

Solution:

Decouple your user interface from your data model so that the destruction of your Activity doesn't affect your state data.

Discussion:

It's a situation that every Android developer runs into with their very first application: "My application works great, but when I change my phone's orientation everything resets!"

By design, when a device's configuration (read: orientation) changes, the Android UI Framework destroys the current Activity and recreates it for the new configuration. This enables the designer to optimize the layout for different screen orientations and sizes. However, this causes a problem for the developer who wishes to maintain the state of the Activity as it was before the orientation change destroyed the screen. Attempting to solve this problem can lead to many complicated solutions, some more graceful than others. But if we take a step back and design our application wisely, we can write cleaner, more robust code that makes life easier for everyone.

A Graphical User Interface (GUI) is exactly what its name describes. It is a graphical representation of an underlying data model that allows the user to interface with and manipulate the data. It is NOT the data model itself. Let's talk our way through an example to illustrate why that is an important point to make.

Consider a Tic-Tac-Toe application. A simple main Activity for this would most likely include [at bare minimum] a GridView (with appropriate Adapter) to display the board and a TextView to tell the user whose turn it is. When the user clicks a square in the grid, an appropriate X or O is placed in that grid cell. As a new Android developer, we find it logical to also include a 2-dimensional array containing a representation of the board to store its data so that we can determine if the game is over, and if so, who won.

public class TicTacToeActivity extends Activity {
    
    private TicTacToeState[][] mBoardState;
    
    private GridView mBoard;
    private TextView mTurnText;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        
        setContentView(R.layout.main);
        
        mBoardState = new TicTacToeState[3][3];
        
        mBoard = (GridView)findViewById(R.id.board);
        mTurnText = (TextView)findViewById(R.id.turn_text);
        
        // ... Set up Adapter, OnClickListeners, etc, for mBoard.
    }
}

This is easy enough to imagine and implement, and everything works great. Except that when you turn your phone sideways in the middle of an intense round of Tic-Tac-Toe, you have a fresh board staring you in the face and your inevitable victory is postponed. As described earlier, the UI Framework just destroyed your Activity and recreated it, calling onCreate() and resetting the board data.

While reading the above code, you might have said to yourself, "Hey, that 'Bundle savedInstanceState' looks promising!" And you'd be right. For this painfully, almost criminally simple example, you could stick your board data into a Bundle and use it to reload your screen. There's even a pair of methods, onRetainNonConfigurationInstance() and getLastNonConfigurationInstance(), that let you pass any Object you want from your old, destroyed Activity, to your newly created one. For this example you could just pass your mBoardState array to your new Activity and you'd be all set. But we're going to write big, successful, amazing apps any day now, and that just doesn't scale well with complicated interfaces. We can do better!

This is why separating your GUI from your data model is so handy. Your GUI can be destroyed, recreated, and changed, but the underlying data can survive unharmed through as many UI changes as you can throw at it. Let's separate our game state out into a separate data class.

public class TicTacToeGame {
    
    private TicTacToeState[][] mBoardState;
    
    public TicTacToeGame() {
        mBoardState = new TicTactoeState[3][3];
        // ... Initialize
    }
    
    public TicTacToeState getCellState(int row, int col) {
        return mBoardState[row][col];
    }
    public void setCellState(int row, int col, TicTacToeState state) {
        mBoardState[row][col] = state;
    }
    
    // ... Other utility methods to determine whose turn it is, if the game is over, etc.
}

This will not only help us maintain our application state, it's generally just good Object Oriented Design.

Now that we have our data safely outside of the volatile Activity, how do we access it to build our interface? There are two common approaches: 1) Declare all variables in TicTacToeGame as static, and access them through static methods. 2) Design TicTacToeGame as a Singleton, allowing access to one global instance to be used throughout our application.

I prefer the second option purely from a design preference perspective. We can turn TicTacToeGame into a Singleton by making the constructor private and adding the following lines to the top of the class:

private static TicTacToeGame instance = new TicTacToeGame();
public static TicTacToeGame getInstance() {
    return instance;
};

Now all we have to do is obtain the game data, and set our UI elements to appropriately display the data. It's most useful to wrap this in its own function - refreshUI(), perhaps - so that it can be used whenever your Activity makes a change to the data. For example, when a user clicks a cell of the board, there need only be two lines of code in the listener: one call to modify the data model (via our TicTacToeGame singleton), and one call to refresh the UI.

It may be obvious, but it is worth mentioning that your data classes survive only as long as your application's process is running. If it is killed by the user or the system, naturally the data is lost. That situation necessitates more persistent storage through the file system or databases and is outside the scope of this recipe.

This approach very effectively decouples your visual representation of the data from the data itself, and makes orientation changes trivial. Simply calling refreshUI() in your onCreate(Bundle) method is enough to ensure that whenever your Activity is destroyed and recreated, it can access the data model and display itself correctly. And as an added bonus, you're now practicing better Object Oriented Design and will see your code base become cleaner, more scalable, and easier to maintain.

No records found.