Friday, November 26, 2010

Flexible layout with RelativeLayout in Android

I often create a layout for my Android applications with a list and horizontal row of buttons on the bottom. Considering that Android devices come in very different sizes, you want to make this layout flexible so that the list takes up the entire area above the row of buttons on the bottom.

You could do this with a LinearLayout and use layout_weight. This often does not behave the way I want to because the LinearLayout is quite limited. I found an easier way to use a RelativeLayout, see the example: 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:layout_width="fill_parent"
      android:layout_height="fill_parent"
      android:orientation="vertical">
    
     <LinearLayout android:id="@+id/button_bar"
          android:orientation="horizontal"
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"
          android:layout_alignParentBottom="true"
          >

        <Button android:id="@+id/add_task_context_button"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:text="@string/task_context_add_button_title"
             />
        <Button android:id="@+id/cancel_button"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:text="@string/cancel_button_title"
             />
        <Button android:id="@+id/ok_button"
             android:layout_width="fill_parent"
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:text="@string/ok_button_title"
             />
     </LinearLayout>

    <ListView android:id="@+id/android:list"
              android:layout_width="fill_parent" 
              android:layout_height="fill_parent"
              android:layout_alignParentTop="true"
              android:layout_above="@id/button_bar"
              android:drawSelectorOnTop="false"
              style="@style/list"  
              />

    <TextView android:id="@+id/android:empty"
              android:layout_width="fill_parent"
              android:layout_height="wrap_content"
              android:layout_alignParentTop="true"
              android:text="@string/no_task_contexts"
              style="@style/label"                
              android:padding="10px"
              />
              

</RelativeLayout>

When you use a RelativeLayout, pay attention to the order of components. Because you specify where components are positioned relative to the parent's borders and other components.

To make my layout work, I first specify the horizontal row of buttons first (button_bar), contained in a LinearLayout with android:layout_alignParentBottom="true". When you position the list on the top with android:layout_alignParentTop="true", you also attach the bottom of the list with the top of the button row with android:layout_above="@id/button_bar". This way, the RelativeLayout will stretch the list between the top of the parent and the top of the button_bar.

Tuesday, March 2, 2010

Handling portrait/landscape switch in Android

When you develop an Android application, you will notice that when the user changes the orientation of the device, the UI framework will recreate the current screen layout with the corresponding UI objects. The framework calls these methods of the current activity: onPause, onStop, onDestroy, and then onCreate, onStart, onResume to display the activity again. The onCreate method of the activity retrieves the data it displays from a database and populates the input fields with the private populateFields method.
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  /* UI setup here */
          
  Uri itemUri = getIntent().getData();
  if (newRecord) {
    customer = new Customer();
  } else {
    customer = retrieveCustomer(itemUri);
  }

  populateFields(customer);
}
When the user switches orientation, the Android framework will destroy this activity and call onCreate again, which will retrieve the data and populate the input fields. The result is that the user will lose entered data, because this input fields will contain what the activity retrieved from the database. A solution is to store the entered data in the database in the onPause method:
protected void onResume() {
  copyFromInput(customer);
  if (newRecord) {
    saveCustomer(customer);
  } else {
    updateCustomer(customer);
  }

  super.onResume();
}
This solution mostly works and also takes care of automatic saving the entered data when the user uses the "home" button. This may also surprise the user, since the data is saved without the user explicitly asking to save it. A better solution is to use the savedInstanceState parameter that the Android framework passes into the onCreate method. This parameter is null when the user navigates to the activity. When the user changes orientation of the Android device, the framework destroys the activity after saving the contents of the input fields in a Bundle object. When the framework recreates the activity after an orientation change, the savedInstanceState parameter is not null and contains all the data that your application would want save during this change. The Android framework already automatically saves and restores the contents of the input fields, so your application does not need to do this. The thing that your application needs to be aware of is that it should only set the contents of the input fields when the savedInstanceState parameter is null.
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  /* UI setup here */
          
  Uri itemUri = getIntent().getData();
  if (newRecord) {
    customer = new Customer();
  } else {
    customer = retrieveCustomer(itemUri);
  }
  
  if (savedInstanceState == null) {
    populateFields(fragment);
  }
}

Friday, January 29, 2010

ID design

We often take the IDs that we use in databases and applications for granted. As long as we can identify a data record with a unique number, everything is great. Recently, I ran into an interesting problem in an existing application. The ID that was used has this format:
ABBBCCC

A = last digit of year
BBB = day number of the year
CCC = sequence number
For example, you could get on 2 february 2009 this ID: 9033012. This would be the twelveth data record of the day.

More digits

Sometimes, the application would generate more than 1000 data records on a day and run out of numbers. In that case the application just add digits to get something like this:
ABBBCCCDDD

A = last digit of year
BBB = day number of the year
CCCDDD = sequence number with additional digits
Example: 9003123456

Perhaps you can sense some trouble here...