Chapter    9

Responding to Configuration Changes

We’ve covered a fair bit of material so far, and now seems like a good time to cover configuration changes. When an application is running on a device, and the device’s configuration changes (for example, is rotated 90 degrees), your application needs to respond accordingly. The new configuration will most likely look different from the previous configuration. For example, switching from portrait to landscape mode means the screen went from being tall and narrow to being short and wide. The UI elements (buttons, text, lists, and so on) will need to be rearranged, resized, or even removed to accommodate the new configuration.

In Android, a configuration change by default causes the current activity to go away and be re-created. The application itself keeps on running, but it has the opportunity to change how the activity is displayed in response to the configuration change. In the rare case that you need to handle a configuration change without destroying and re-creating your activity, Android provides a way to handle that as well.

Be aware that configuration changes can take on many forms, not just device rotation. If a device gets connected to a dock, that’s also a configuration change. So is changing the language of the device. Whatever the new configuration is, as long as you’ve designed your activity for that configuration, Android takes care of most everything to transition to it, giving the user a seamless experience.

This chapter will take you through the process of a configuration change, from the perspectives of both activities and fragments. We’ll show you how to design your application for those transitions and how to avoid traps that could cause your application to crash or misbehave.

The Default Configuration Change Process

The Android operating system keeps track of the current configuration of the device it’s running on. Configuration includes lots of factors, and new ones get added all the time. For example, if a device is plugged into a docking station, that represents a change in the device configuration. When a configuration change is detected by Android, callbacks are invoked in running applications to tell them a change is occurring, so an application can properly respond to the change. We’ll discuss those callbacks a little later, but for now let’s refresh your memory with regard to resources.

One of the great features of Android is that resources get selected for your activity based on the current configuration of the device. You don’t need to write code to figure out which configuration is active; you just access resources by name, and Android gets the appropriate resources for you. If the device is in portrait mode and your application requests a layout, you get the portrait layout. If the device is in landscape mode, you get the landscape layout. The code just requests a layout without specifying which one it should get. This is powerful because as new configuration factors get introduced, or new values for configuration factors, the code stays the same. All a developer needs to do is decide if new resources need to be created, and create them as necessary for the new configuration. Then, when the application goes through a configuration change, Android provides the new resources to the application, and everything continues to function as desired.

Because of a great desire to keep things simple, Android destroys the current activity when the configuration changes and creates a new one in its place. This might seem rather harsh, but it’s not. It is a bigger challenge to take a running activity and figure out which parts would stay the same and which would not, and then only work with the pieces that need to change.

An activity that’s about to be destroyed is properly notified first, giving you a chance to save anything that needs to be saved. When the new activity gets created, it has the opportunity to restore state using data from the previous activity. For a good user experience, obviously you do not want this save and restore to take very long.

It’s fairly easy to save any data that you need saved and then let Android throw away the rest and start over, as long as the design of the application and its activities is such that activities don’t contain a lot of non-UI stuff that would take a long time to re-create. Therein lies the secret to successful configuration change design: do not put “stuff” inside an activity that cannot be easily re-created during a configuration change.

Keep in mind that our application is not being destroyed, so anything that is in the application context, and not a part of our current activity, will still be there for the new activity. Singletons will still be available, as well as any background threads we might have spun off to do work for our application. Any databases or content providers that we were working with will also still be around. Taking advantage of these makes configuration changes quick and painless. Keep data and business logic outside of activities if you can.

The configuration change process is somewhat similar between activities and fragments. When an activity is being destroyed and re-created, the fragments within that activity get destroyed and re-created. What we need to worry about then is state information about our fragments and activity, such as data currently being displayed to the user, or internal values that we want to preserve. We will save what we want to keep, and pick it up again on the other side when the fragments and activities are being re-created. You’ll want to protect data that can’t easily be re-created by not letting it get destroyed in the default configuration change process.

The Destroy/Create Cycle of Activities

There are three callbacks to be aware of when dealing with default configuration changes in activities:

  • onSaveInstanceState()
  • onCreate()
  • onRestoreInstanceState()

The first is the callback that Android will invoke when it detects that a configuration change is happening. The activity has a chance to save state that it wants to restore when the new activity gets created at the end of the configuration change. The onSaveInstanceState() callback will be called prior to the call to onStop(). Whatever state exists can be accessed and saved into a Bundle object. This object will get passed in to both of the other callbacks (onCreate() and onRestoreInstanceState()) when the activity is re-created. You only need to put logic in one or the other to restore your activity’s state.

The default onSaveInstanceState() callback does some nice things for you. For example, it goes through the currently active view hierarchy and saves the values for each view that has an android:id. This means if you have an EditText view that has received some user input, that input will be available on the other side of the activity destroy/create cycle to populate the EditText before the user gets control back. You do not need to go through and save this state yourself. If you do override onSaveInstanceState(), be sure to call super.onSaveInstanceState() with the bundle object so it can take care of this for you. It’s not the views that are saved, only the attributes of their state that should persist across the destroy/create boundary.

To save data in the bundle object, use methods such as putInt() for integers and putString() for strings. There are quite a few methods in the android.os.Bundle class; you are not limited to integers and strings. For example, putParcelable() can be used to save complex objects. Each put is used with a string key, and you will retrieve the value later using the same key used to put the value in. A sample onSaveInstanceState() might look like Listing 9-1.

Listing 9-1. Sample onSaveInstanceState()

@Override
public void onSaveInstanceState(Bundle icicle) {
    super.onSaveInstanceState(icicle);
    icicle.putInt("counter", 1);
}

Sometimes the bundle is called icicle because it represents a small frozen piece of an activity. In this sample, you only save one value, and it has a key of counter. You could save more values by simply adding more put statements to this callback. The counter value in this example is somewhat temporary because if the application is completely destroyed, the current value will be lost. This could happen if the user turned off their device, for example. In Chapter 11, you’ll learn about ways to save values more permanently. This instance state is only meant to hang onto values while the application is running this time. Do not use this mechanism for state that is important to keep for a longer term.

To restore activity state, you access the bundle object to retrieve values that you believe are there. Again, you use methods of the Bundle class such as getInt() and getString() with the appropriate key passed to tell which value you want back. If the key does not exist in the Bundle, a value of 0 or null is passed back (depending on the type of the object being requested). Or you can provide a default value in the appropriate getter method. Listing 9-2 shows a sample onRestoreInstanceState() callback.

Listing 9-2. Sample onRestoreInstanceState()

@Override
public void onRestoreInstanceState(Bundle icicle) {
    super.onRestoreInstanceState(icicle);
    int someInt = icicle.getInt("counter", -1);
    // Now go do something with someInt to restore the
    // state of the activity. -1 is the default if no
    // value was found.
}

It’s up to you whether you restore state in onCreate() or in onRestoreInstanceState(). Many applications will restore state in onCreate() because that is where a lot of initialization is done. One reason to separate the two would be if you’re creating an activity class that could be extended. The developers doing the extending might find it easier to just override onRestoreInstanceState() with the code to restore state, as compared to having to override all of onCreate().

What’s very important to note here is that you need to be very concerned with references to activities and views and other objects that need to be garbage-collected when the current activity is fully destroyed. If you put something into the saved bundle that refers back to the activity being destroyed, that activity can’t be garbage collected. This is very likely a memory leak that could grow and grow until your application crashes. Objects to avoid in bundles include Drawables, Adapters, Views, and anything else that is tied to the activity context. Instead of putting a Drawable into the bundle, serialize the bitmap and save that. Or better yet, manage the bitmaps outside of the activity and fragment instead of inside. Add some sort of reference to the bitmap to the bundle. When it comes time to re-create any Drawables for the new fragment, use the reference to access the outside bitmaps to regenerate your Drawables.

The Destroy/Create Cycle of Fragments

The destroy/create cycle for fragments is very similar to that of activities. A fragment in the process of being destroyed and re-created will have its onSaveInstanceState() callback called, allowing the fragment to save values in a Bundle object for later. One difference is that six fragment callbacks receive this Bundle object when a fragment is being re-created: onInflate(), onCreate(), onCreateView(), onActivityCreated(), onViewCreated(), and onViewStateRestored(). The last two callbacks are more recent, from Honeycomb 3.2 and JellyBean 4.2 respectively. This gives us lots of opportunities to rebuild the internal state of our reconstructed fragment from its previous state.

Android guarantees only that onSaveInstanceState() will be called for a fragment sometime before onDestroy(). That means the view hierarchy may or may not be attached when onSaveInstanceState() is called. Therefore, don’t count on traversing the view hierarchy inside of onSaveInstanceState(). For example, if the fragment is on the fragment back stack, no UI will be showing, so no view hierarchy will exist. This is OK of course because if no UI is showing, there is no need to attempt to capture the current values of views to save them. You need to check if a view exists before trying to save its current value, and not consider it an error if the view does not exist.

Just like with activities, be careful not to include items in the bundle object that refer to an activity or to a fragment that might not exist later when this fragment is being re-created. Keep the size of the bundle as small as possible, and as much as possible store long-lasting data outside of activities and fragments and simply refer to it from your activities and fragments. Then your destroy/create cycles will go that much faster, you’ll be much less likely to create a memory leak, and your activity and fragment code should be easier to maintain.

Using FragmentManager to Save Fragment State

Fragments have another way to save state, in addition to, or instead of, Android notifying the fragments that their state should be saved. With Honeycomb 3.2, the FragmentManager class got a saveFragmentInstanceState() method that can be called to generate an object of the class Fragment.SavedState. The methods mentioned in the previous sections for saving state do so within the internals of Android. While we know that the state is being saved, we do not have any direct access to it. This method of saving state gives you an object that represents the saved state of a fragment and allows you to control if and when a fragment is created from that state.

The way to use a Fragment.SavedState object to restore a fragment is through the setInitialSavedState() method of the Fragment class. In Chapter 8, you learned that it is best to create new fragments using a static factory method (for example, newInstance()). Within this method, you saw how a default constructor is called and then an arguments bundle is attached. You could instead call the setInitialSavedState() method to set it up for restoration to a previous state.

There are a few caveats you should know about this method of saving fragment state:

  • The fragment to be saved must currently be attached to the fragment manager.
  • A new fragment created using this saved state must be the same class type as the fragment it was created from.
  • The saved state cannot contain dependencies on other fragments. Other fragments may not exist when the saved fragment is re-created.

Using setRetainInstance on a Fragment

A fragment can avoid being destroyed and re-created on a configuration change. If the setRetainInstance() method is called with an argument of true, the fragment will be retained in the application when its activity is being destroyed and re-created. The fragment’s onDestroy() callback will not be called, nor will onCreate(). The onDetach() callback will be called because the fragment must be detached from the activity that’s going away, and onAttach() and onActivityCreated() will be called because the fragment is attached to a new activity. This only works for fragments that are not on the back stack. It is especially useful for fragments that do not have a UI.

This feature is very powerful in that you can use a non-UI fragment to handle references to your data objects and background threads, and call setRetainInstance(true) on this fragment so it won’t get destroyed and re-created on a configuration change. The added bonus is that during the normal configuration change process, the non-UI fragment callbacks onDetach() and onAttach() will switch the activity reference from the old to the new.

Deprecated Configuration Change Methods

A couple of methods on Activity have been deprecated, so you should no longer use them:

  • getLastNonConfigurationInstance()
  • onRetainNonConfigurationInstance()

These methods previously allowed you to save an arbitrary object from an activity that was being destroyed, to be passed to the next instance of the activity that was being created. Although they were useful, you should now use the methods described earlier instead to manage data between instances of activities in the destroy/create cycle.

Handling Configuration Changes Yourself

So far, you’ve seen how Android handles configuration changes for you. It takes care of destroying and re-creating activities and fragments, pulling in the best resources for the new configuration, retaining any user-entered data, and giving you the opportunity to execute some extra logic in some callbacks. This is usually going to be your best option. But when it isn’t, when you have to handle a configuration change yourself, Android provides a way out. This isn’t recommended because it is then completely up to you to determine what needs to change due to the change, and then for you to take care of making all the changes. As mentioned before, there are many configuration changes besides just an orientation change. Luckily, you don’t necessarily have to handle all configuration changes yourself.

The first step to handling configuration changes yourself is to declare in the <activity> tag in AndroidManifest.xml file which changes you’re going to handle using the android:configChanges attribute. Android will handle the other configuration changes using the previously described methods. You can specify as many configuration change types as needed by or’ing them together with the ‘|’ symbol, like this:

<activity  ...   android:configChanges="orientation|keyboardHidden" ... >

The complete list of configuration change types can be found on the reference page for R.attr. Be aware that if you target API 13 or higher and you need to handle orientation, you also need to handle screenSize.

The default process for a configuration change is the invoking of callbacks to destroy and re-create the activity or fragment. When you’ve declared that you will handle the specific configuration change, the process changes so only the onConfigurationChanged() callback is invoked instead, on the activity and its fragments. Android passes in a Configuration object so the callback knows what the new configuration is. It is up to the callback to determine what might have changed; however, since you likely handle only a small number of configuration changes yourself, it shouldn’t be too hard to figure this out.

You’d really only want to handle a configuration change yourself when there is very little to be done, when you could skip destroying and re-creating. For example, if the activity layout for portrait and landscape is the same layout and all image resources are the same, destroying and re-creating the activity doesn’t really accomplish anything. In this case it would be fairly safe to declare that you will handle the orientation configuration change. During an orientation change of your activity, the activity would remain intact and simply re-render itself in the new orientation using the existing resources such as the layout, images, strings, etc. But it’s really not that big a deal to just let Android take care of things if you can.

References

Here are some helpful references to topics you may wish to explore further:

Summary

Let’s conclude this chapter by quickly enumerating what you have learned about handling configuration changes:

  • Activities by default get destroyed and re-created during configuration changes. So do fragments.
  • Avoid putting lots of data and logic into activities so configuration changes occur quickly.
  • Let Android provide the appropriate resources.
  • Use singletons to hold data outside of activities to make it easier to destroy and re-create activities during configuration changes.
  • Take advantage of the default onSaveInstanceState() callback to save UI state on views with android:ids.
  • If a fragment can survive with no issues across an activity destroy-and-create cycle, use setRetainInstance() to tell Android it doesn’t need to destroy and create the fragment.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.190.160.63