A few weeks back, I wrote about how rotations, including sliding out the keyboard on the G1, affect your Android application. Ed Burnette chimed in with a couple of other points in a blog comment, so I wanted to expand upon what he presented there.
The original post focused on the fact that a rotation, by default, will destroy and recreate your activity, no different than if the activity were reclaimed due to low memory space or other conditions. Hence, in theory, whatever logic you wrote to handle being destroyed to release memory would also cover you for a rotation. Usually, this involves onSaveInstanceState(), so you can persist your current state and get it back in onCreate() or onRestoreInstanceState().
Today, we’ll cover a second approach — onRetainNonConfigurationInstance() — in an adapted excerpt from Version 1.4 of The Busy Coder’s Guide to Android Development.
The problem with onSaveInstanceState() is that you are limited to a Bundle. That’s because this callback is also used in cases where your whole process might be terminated (e.g., low memory), so the data to be saved has to be something that can be serialized and has no dependencies upon your running process.
For some activities, that limitation is not a problem. For others, though, it is more annoying. Take an online chat, for example. You have no means of storing a socket in a Bundle, so by default, you will have to drop your connection to the chat server and re-establish it. That not only may be a performance hit, but it might also affect the chat itself, such as you appearing in the chat logs as disconnecting and reconnecting.
One way to get past this is to use onRetainNonConfigurationInstance() instead of onSaveInstanceState() for “light” changes like a rotation. Your activity’s onRetainNonConfigurationInstance() callback can return an Object, which you can retrieve later via getLastNonConfigurationInstance(). The Object can be just about anything you want – typically, it will be some kind of “context” object holding activity state, such as running threads, open sockets, and the like. Your activity’s onCreate() can call getLastNonConfigurationInstance() – if you get a non-null response, you now have your sockets and threads and whatnot. The biggest limitation is that you do not want to put in the saved context anything that might reference a resource that will get swapped out, such as a Drawable loaded from a resource.
Let’s look at an example. Below is a layout for a simple UI with two buttons:
[sourcecode language=’xml’]
[/sourcecode]
The idea is that when the Pick button is clicked, the user can pick a contact out of their on-device contact list. At that point, the View button is enabled — clicking it will display the contact in question. What we want is for the application to survive a rotation, so if the user picked a contact with the phone in portrait mode, then rotated it to landscape, the View button should still be enabled and the contact be remembered. There is a separate layout for the landscape perspective, with the two buttons side-by-side rather than vertically stacked.
Here is some source code that implements this logic:
[sourcecode language=’java’]
package com.commonsware.android.rotation.two;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.util.Log;
public class RotationTwoDemo extends Activity {
static final int PICK_REQUEST=1337;
Button viewButton=null;
Uri contact=null;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button btn=(Button)findViewById(R.id.pick);
btn.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
Intent i=new Intent(Intent.ACTION_PICK,
Uri.parse(“content://contact/people”));
startActivityForResult(i, PICK_REQUEST);
}
});
viewButton=(Button)findViewById(R.id.view);
viewButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
startActivity(new Intent(Intent.ACTION_VIEW, contact));
}
});
restoreMe();
viewButton.setEnabled(contact!=null);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode==PICK_REQUEST) {
if (resultCode==RESULT_OK) {
contact=data.getData();
viewButton.setEnabled(true);
}
}
}
@Override
public Object onRetainNonConfigurationInstance() {
return(contact);
}
private void restoreMe() {
contact=null;
if (getLastNonConfigurationInstance()!=null) {
contact=(Uri)getLastNonConfigurationInstance();
}
}
}
[/sourcecode]
Here, we override onRetainNonConfigurationInstance(), returning the actual Uri for our contact, rather than a string representation of it. In turn, restoreMe() calls getLastNonConfigurationInstance(), and if it is not null, we hold onto it as our contact and enable the “View” button.