Bacon Rank Android App Details
Bacon Rank is currently my most ambitious Android project from UI and background processing point of view. It has custom UI controls and it deals gracefully with long running network operations.
Let’s start with the custom SeekBar first, which is used to select the number of strips of bacon you ate. When you have a touch screen device, especially with no physical keyboard, you want to avoid situations where the user is forced to type anything. In my case SeekBar
works great, since you can’t eat that many strips of bacon at one sitting. To draw the SeekBar
with custom images I created bacon_seekbar.xm
l in the drawable
folder:
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" > <item android:id="@+android:id/background" android:drawable="@drawable/progress_mediumbacon" /> <item android:id="@+android:id/SecondaryProgress" android:drawable="@drawable/progress_rawbacon" /> <item android:id="@+android:id/progress" android:drawable="@drawable/progress_cookedbacon" /> </layer-list>
The progress_*
drawables are PNG images.
In the main layout I use the custom SeekBar
and set a custom thumb (a PNG image of a skillet):
<SeekBar android:id="@+id/SEEKBAR" android:layout_below="@id/BACON_STRIPS" android:layout_width="fill_parent" android:layout_height="wrap_content" android:max="20" android:progress="0" android:secondaryProgress="0" android:paddingLeft="32px" android:paddingRight="32px" android:progressDrawable="@drawable/bacon_seekbar" android:thumb="@drawable/skillet" />
SeekBar
has three states (if you count background), but I am really just using two. The weird part is that I thought I would use background
and progress
states, but I could not get that to work. Instead, I am using SecondaryProgress
as background, and background is not visible anywhere. In onCreate
I set up the SeekBar
:
mSeekBar = (SeekBar)findViewById(R.id.SEEKBAR); mSeekBar.setOnSeekBarChangeListener(this); mSeekBar.setMax(PROGRESS_MAX);
In onProgressChanged
method I update the amount in the UI and that is about it for the SeekBar
. I think it looks pretty nice, but it would be better if I could make it look more 3D:
The trickiest part of the application was handling of the network activity while allowing for screen orientation changes. None of the tutorials I’ve read or the stackoverflow questions and answers regarding this seemed workable (or maybe I did not understand them). The problem is this: suppose in your activity you start an AsyncTask for the network activity. This is what is actually recommended pretty much everywhere. But suppose your user then changes the screen orientation. By default Android destroys and recreates the current activity. Your AsyncTask
is still going, but it does not know about the new activity, and your new activity has no knowledge of the AsyncTask
. So what is the fix?
It turns out you can create a custom application object, which will be available as long as your process is running. So in your activity you can start the AsyncTask
as before, but keep a reference to the AsyncTask
in the application object. The AsyncTask
needs a reference to the current activity to draw on the screen, which your activity keeps up to date. Basically set it on AsyncTask
creation, null it during screen orientation and activity destroy, and restore when the activity is recreated. We also want to run an indeterminate progress indicator in the UI while network activity is going. In code this looks something like this (I’m omitting many irrelevant details):
public class BaconApp extends Application { public BaconRank.SyncTask mSyncTask = null; // ... } public class BaconRank extends Activity implements SeekBar.OnSeekBarChangeListener { // ... @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); // ... } BaconRank.SyncTask getSyncTask() { return ((BaconApp)getApplication()).mSyncTask; } void sync(String action) { ((BaconApp)getApplication()).mSyncTask = (SyncTask) new SyncTask(this).execute(getUserAgent(), mUserId, mBaconStrips.getText().toString(), action); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); SyncTask syncTask = getSyncTask(); if (syncTask != null) { syncTask.setActivity(null); } } @Override public void onRestoreInstanceState(Bundle inState) { super.onRestoreInstanceState(inState); SyncTask syncTask = getSyncTask(); if (syncTask != null) { setProgressBarIndeterminateVisibility(true); syncTask.setActivity(this); } } @Override public void onStop() { super.onStop(); SyncTask syncTask = getSyncTask(); if (syncTask != null) { syncTask.setActivity(null); } } public class SyncTask extends AsyncTask<String, Void, JSONObject> { // ... public SyncTask(BaconRank activity) { super(); mActivity = activity; } public void setActivity(BaconRank activity) { mActivity = activity; } protected void onPreExecute() { if (mActivity != null) { mActivity.setProgressBarIndeterminateVisibility(true); } } protected void onCancelled() { // mActivity cannot be null mActivity.setProgressBarIndeterminateVisibility(false); clearActivity(); } void clearActivity() { // call in ui thread only ((BaconApp)mActivity.getApplication()).mSyncTask = null; } protected void onPostExecute(JSONObject result) { if (mActivity != null) { // Update UI } clearActivity(); } protected JSONObject doInBackground(String... params) { // Do the network stuff } // ... }
You will also need to specify the custom application object in the AndroidManifest.xml:
<application android:name=".BaconApp" >
And that wraps the Bacon Rank presentation. If you know how to do anything I presented above in a better way, please leave a comment. The application is also localizeable, so if you want to translate the app into your language let me know.
If you did not read it already, you might want to read about the application running baconrank.com, the server the Android application talks to.