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

Program: Tipster, a Tip Calculator for the Android OS

Author: Sunit Katkar -- Published? true -- FormatLanguage: W

Problem:

A tip calculator is quite a simple application. When you go with friends to a restaurant and wish to divide the check and tip, you simply add the tip percentage to the total and divide by the number of diners. Tipster is an implementation of this in Android, to show a complete application.

Solution:

This is a simple exercise to use the basic GUI elements in Android and then piecing them together with some simple calculation and some event driven UI code to tie it all together. We will use the following GUI components:

  • TableLayout - provides a good control over screen layout. This layout allows us to use the HTML Table tag paradigm to layout widgets
  • TableRow - this defines a row in the TableLayout. Its like the HTML TR and TD tag combined.
  • TextView - this View provides a label for displaying static text on the screen
  • EditText - this View provides a text field for entering values.
  • RadioGroup - this groups together radio buttons
  • RadioButton - this provides a radio button
  • Button - this is the regular button
  • View - we will use a View to create a visual separator with certain height and color attributes

Discussion:

Android uses XML files for the Layout of widgets. In our example project, the Android plugin for Eclipse generates a main.xml file for the layout. This file has the XML based definitions of the different widgets and their containers.

There is a strings.xml file which has all the string resources used in the application. A default icon.png file is provided for the application icon.

Then there is the R.java file which is automatically generated (and updated when any changes are made to main.xml). This file has the constants defined for each of the layout and widget. Do not edit this file manually. The plugin is does it for you when you do a clean build.

In our example we have Tipster.java as the main Java file or the Activity.

Google tutorials highlight how to use the plugin. Using the Eclipse plugin, create an Android project named Tipster. The end result will be a project layout like the following screen shot.

Creating the Layout and placing the Widgets The end goal is to create a layout as shown in the following screen shot.

For this screen layout we will use the following layouts and widgets:

  • TableLayout - provides a good control over screen layout. This layout allows us to use the HTML Table tag paradigm to layout widgets
  • TableRow - this defines a row in the TableLayout. Its like the HTML TR and TD tag combined.
  • TextView - this View provides a label for displaying static text on the screen
  • EditText - this View provides a text field for entering values.
  • RadioGroup - this groups together radio buttons
  • RadioButton - this provides a radio button
  • Button - this is the regular button
  • View - we will use a View to create a visual separator with certain height and color attributes

Familiarize yourself with these widgets as you will be using these quite a lot in applications you build. When you go to the Javadocs for each of the above, do look up the XML attributes. This will help you correlate the usage in the main.xml layout file and the Java code (Tipster.java and R.java) where these are accessed.

There is a visual editor in the Eclipse ADT as well as a standalone UI tool Droid Draw, both of which allow you create a layout by drag-and-drop of widgets from a palette, like any form designer tool. However, we recommend that you create the layout by hand in XML, at least in your initial stages of learning Android. Later on you, as you learn all the nuances of the XML layout API, you can delegate the task to such tools.

The Layout file - main.xml - has the layout information. I have posted the file below. The source code comments make the file quite self-explanatory.

A TableRow widget creates a single row inside the TableLayout. So we use as many TableRows as the number of rows we want. In this tutorial we use 8 TableRows - 5 for the widgets till the visual separator below the buttons and 3 for the results area below the buttons and separator.

/res/layout/main.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Using table layout to have HTML table like control over layout -->
<TableLayout
        android:id="@+id/TableLayout01"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:stretchColumns="1"
        xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Row 1: Text label placed in column zero,
         text field placed in column two and allowed to
         span two columns. So a total of 4 columns in this row -->
        <TableRow>
        <TextView
                android:id="@+id/txtLbl1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:text="@string/textLbl1"/>
        <EditText
                android:id="@+id/txtAmount"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:numeric="decimal"
                android:layout_column="2"
                android:layout_span="2"
                />                     
        </TableRow>
    <!-- Row 2: Text label placed in column zero,
         text field placed in column two and allowed to
         span two columns. So a total of 4 columns in this row -->
        <TableRow>
        <TextView
                android:id="@+id/txtLbl2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:text="@string/textLbl2"/>
        <EditText
                android:id="@+id/txtPeople"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:numeric="integer"
                android:layout_column="2"
                android:layout_span="2"/>                      
        </TableRow>
   <!-- Row 3: This has just one text label placed in column zero  -->
        <TableRow>
        <TextView
                android:id="@+id/txtLbl3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/textLbl3"/>
        </TableRow>    
   <!-- Row 4: RadioGroup for RadioButtons placed at column zero
        with column span of three, thus creating one radio button
        per cell of the table row. Last cell number 4 has the
        textfield to enter a custom tip percentage -->
        <TableRow>     
        <RadioGroup
                android:id="@+id/RadioGroupTips"
                android:orientation="horizontal"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:layout_span="3"
                android:checkedButton="@+id/radioFifteen">
                <RadioButton android:id="@+id/radioFifteen"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/rdoTxt15"
                        android:textSize="15sp" />
                <RadioButton android:id="@+id/radioTwenty"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/rdoTxt20"
                        android:textSize="15sp" />
                <RadioButton android:id="@+id/radioOther"
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/rdoTxtOther"
                        android:textSize="15sp" />
        </RadioGroup>
                <EditText
                        android:id="@+id/txtTipOther"
                        android:layout_width="fill_parent"
                        android:layout_height="wrap_content"
                        android:numeric="decimal"/>                    
        </TableRow>
   <!--  Row for the Calculate and Rest buttons. The Calculate button
         is placed at column two, and Reset at column three -->         
        <TableRow>
        <Button
                android:id="@+id/btnReset"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="2"  
                android:text="@string/btnReset"/>
        <Button
                android:id="@+id/btnCalculate"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="3"
                android:text="@string/btnCalculate"/>
        </TableRow>    
 
    <!-- TableLayout allows any other views to be inserted between
         the TableRow elements. So inserting a blank view to create a
         line separator. This separator view is used to separate
         the area below the buttons which will display the
         calculation results -->
        <View
                android:layout_height="2px"
                android:background="#DDFFDD"
                android:layout_marginTop="5dip"
                android:layout_marginBottom="5dip"/>
 
    <!-- Again table row is used to place the result textviews
         at column zero and the result in textviews at column two -->
        <TableRow android:paddingBottom="10dip" android:paddingTop="5dip">
        <TextView
                android:id="@+id/txtLbl4"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:text="@string/textLbl4"/>
        <TextView
                android:id="@+id/txtTipAmount"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="2"
                android:layout_span="2"/>                      
        </TableRow>
       
        <TableRow android:paddingBottom="10dip" android:paddingTop="5dip">
        <TextView
                android:id="@+id/txtLbl5"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:text="@string/textLbl5"/>
        <TextView
                android:id="@+id/txtTotalToPay"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="2"
                android:layout_span="2"/>                      
        </TableRow>
       
        <TableRow android:paddingBottom="10dip" android:paddingTop="5dip">
        <TextView
                android:id="@+id/txtLbl6"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="0"
                android:text="@string/textLbl6"/>
        <TextView
                android:id="@+id/txtTipPerPerson"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_column="2"
                android:layout_span="2"/>                      
        </TableRow>
    <!--  End of all rows and widgets -->  
</TableLayout>

TableLayout and TableRow After examining the main.xml, you can gather that the TableLayout and TableRow are straightforward to use. You create the TableLayout once, then insert a TableRow. Now you are free to insert any other widgets like TextView, EditView, etc. inside this TableRow.

Do look at the attributes, especially android:stretchColumns, android:layout_column, android:layout_span which allow widget placement like the way you would use use a regular HTML table. I recommend that you follow the links to these attributes and read up on how they work for a TableLayout.

Controlling input values Look at the EditText widget in the main.xml file at line 19. This is the first text field for entering the 'Total Amount' of the check. We want only numbers here. We can accept decimal numbers because real restaurant checks can be for dollars and cents, and not just dollars. So we use the android:numeric attribute with a value of decimal. So this will allow whole values like 10 and decimal values 10.12, but will prevent any other type of entry.

This is a simple and concise way to control input values, thus achieving two things, saving us the trouble of writing validation code in the java file Tipster.java, and ensuring that the user does not enter erroneous values. This XML based constraints feature of Android is quite powerful and useful. You should explore all possible attributes that go with a particular widget to extract maximum benefits from this XML shorthand way of setting constraints. In a future release, unless I have missed it completely in this relase, I wish that Android allows for entering ranges for the adroid:numeric attribute, so that we can define what range of numbers we wish to accept.

Since ranges are not currently available (to the best of my knowledge), you will see later on that we do have to check for certain values like zero or empty values to ensure our tip calculation arithmetic does not fail.

Examining Tipster.java Next we look at the Tipster.java file which controls our application. This is the main class which does the layout, the event handling and the application logic.

Application code - Tipster.java The Android Eclipse plugin creates the Tipster.java file in our project with default code as follows -

Code Snippet 1 of /src/com/examples/tipcalc/Tipster.java

package com.examples.tipcalc;
 
import android.app.Activity;
 
public class Tipster extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }  
}

The Tipster class extends the android.app.Activity class. An activity is a single, focused thing that the user can do. The Activity class takes care of creating the window and then laying out the UI. You have to call the setContentView(View view) method to put your UI in the Activity. So think of Activity as a outer frame which is empty, and you populate it with your UI.

Now look at the snippet of the Tipster.java class. First we define the widgets as class members. Look at lines 3 to 12 of the code snippet 2 below for reference.

Then we use the findViewById(int id) method to locate the widgets. The ID of each widget, defined in your main.xml file, is automatically defined in the R.java file when you clean and build the project in Eclipse. (If you have set up Eclipse to Build Automatically, then the R.java file is instantaneously updated when you update main.xml)

Each widget is derived from the View class, and provides special graphical user interface features. So a TextView provides a way to put labels on the UI, while the EditText provides a text field. Look at lines 24 to 41 in the code snippet 2 below. You can see how findViewById() is used to locate the widgets.

Code Snippet 2 of /src/com/examples/tipcalc/Tipster.java

public class Tipster extends Activity {
    // Widgets in the application
    private EditText txtAmount;
    private EditText txtPeople;
    private EditText txtTipOther;
    private RadioGroup rdoGroupTips;
    private Button btnCalculate;
    private Button btnReset;
 
    private TextView txtTipAmount;
    private TextView txtTotalToPay;
    private TextView txtTipPerPerson;
 
    // For the id of radio button selected
    private int radioCheckedId = -1;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
 
        // Access the various widgets by their id in R.java
        txtAmount = (EditText) findViewById(R.id.txtAmount);
        //On app load, the cursor should be in the Amount field  
        txtAmount.requestFocus();
       
        txtPeople = (EditText) findViewById(R.id.txtPeople);
        txtTipOther = (EditText) findViewById(R.id.txtTipOther);
 
        rdoGroupTips = (RadioGroup) findViewById(R.id.RadioGroupTips);
 
        btnCalculate = (Button) findViewById(R.id.btnCalculate);
        //On app load, the Calculate button is disabled
        btnCalculate.setEnabled(false);
 
        btnReset = (Button) findViewById(R.id.btnReset);
 
        txtTipAmount = (TextView) findViewById(R.id.txtTipAmount);
        txtTotalToPay = (TextView) findViewById(R.id.txtTotalToPay);
        txtTipPerPerson = (TextView) findViewById(R.id.txtTipPerPerson);
 
        // On app load, disable the 'Other tip' percentage text field
        txtTipOther.setEnabled(false);

Addressing 'ease of use' or Usability concerns Our application must try to be as usable as any other established application or web page. In short, adding Usability features will give a good user experience. To address these concerns look at the code snippet 2 again.

Look at line 26 where we use the method requestFocus() of the View class. Since EditText widget is derived from the View class, this method is applicable to it. This is done to so that when our application loads, the 'Total Amount' text field will receive focus and the cursor will be placed in it. This is similar to popular web application login screens where the cursor is present in the username textfield.

Now look at line 35 where the 'Calculate' button is disabled by calling the setEnabled(boolean enabled) method on the Button widget. This is done so that the user cannot click on it before entering values in the required fields. If we allowed the user to click Calculate without entering values in the 'Total Amount' and 'No. of People' fields, then we would have to write validation code to catch these conditions. This would entail showing an alert popup warning the user about the empty values. This adds unnecessary code and user interaction. When the user sees the 'Calculate' button disabled, its quite obvious that unless all values are entered, the tip cannot be calculated.

Look at line 44 in the code snippet 2. Here the 'Other Percentage' text field is disabled. This is done because the '15% tip' radio button is selected by default when the application loads. This default selection on application load is done via the main.xml file. Look at line 66 of main.xml where the following statement selects the '15% tip' radio button.

android:checkedButton="@+id/radioFifteen"

The RadioGroup attribute android:checkedButton allows you to select one of the RadioButton widgets in the group by default.

Most users, who have used popular applications on the desktop as well as the web, are familiar with the 'disabled widgets enabled on certain conditions' paradigm..

Adding such small conveniences always makes the application more usable and the user experience richer.

Processing UI events Like popular Windows, Java Swing, FLex, etc. UI frameworks, Android too provides an Event model which allows to listen to certain events in the UI caused by user interaction. Let us see how we can use the Android event model in our application.

Listening to radio buttons First let us focus on the radio buttons in the UI. We want to know which radio button was selected by the user, as this will allow us to determine the 'Tip Percentage' in our calculations. To 'listen' to radio buttons, we use the static interface OnCheckedChangeListener(). This will notify us when the selection state of a radio button changes.

In our application, we want to enable the 'Other Tip' text field only when the 'Other' radio button is selected. When the 15% and 20% buttons are selected we want to disable this text field. Besides this, we want to add some more logic for sake of usability. As discussed before, we should not enable the 'Calculate' button till all required fields have valid values. In case of the three radio buttons, we want to ensure that the Calculate button gets enabled for the following two conditions -

Other radio button is selected and the 'Other Tip Percentage' text field has valid values 15% or 20% radio button is selected and 'Total Amount' and 'No. Of People' text fields have valid values. Look at the code snippet 3 which deals with the radio buttons. The source code comments are quite self explanatory.

Code Snippet 3 of /src/com/examples/tipcalc/Tipster.java

  /*
   * Attach a OnCheckedChangeListener to the
   * radio group to monitor radio buttons selected by user
   */
   rdoGroupTips.setOnCheckedChangeListener(new OnCheckedChangeListener() {
 
   @Override
   public void onCheckedChanged(RadioGroup group, int checkedId) {
      // Enable/disable Other Percentage tip field
     if (checkedId == R.id.radioFifteen
                || checkedId == R.id.radioTwenty) {
         txtTipOther.setEnabled(false);
         /*
          * Enable the calculate button if Total Amount and No. of
          * People fields have valid values.
          */
         btnCalculate.setEnabled(txtAmount.getText().length() > 0
                   && txtPeople.getText().length() > 0);
     }
     if (checkedId == R.id.radioOther) {
        // enable the Other Percentage tip field
        txtTipOther.setEnabled(true);
        // set the focus to this field
        txtTipOther.requestFocus();
        /*
         * Enable the calculate button if Total Amount and No. of
         * People fields have valid values. Also ensure that user
         * has entered a Other Tip Percentage value before enabling
         * the Calculate button.
         */
        btnCalculate.setEnabled(txtAmount.getText().length() > 0
                && txtPeople.getText().length() > 0
                && txtTipOther.getText().length() > 0);
     }
     // To determine the tip percentage choice made by user
     radioCheckedId = checkedId;
    }
  });

Monitoring key activity in text fields As I mentioned earlier, the 'Calculate' button must not be enabled unless the text fields have valid values. So we have to ensure that the Calculate button will be enabled only if the 'Total Amount', 'No. of People' and 'Other' tip percentage text fields have valid values. The Other tip percentage text field is enabled only if the Other Tip Percentage radio button is selected.

We do not have to worry about the type of values, i.e. whether user entered negative values or alphabetical characters because the android:numeric attribute has been defined for the text fields, thus limiting the types of values that the user can enter. We have to just ensure that the values are present.

So we use the static interface OnKeyListener(). This will notify us when a key is pressed. The notification reaches us before the actual key pressed is sent to the EditText widget.

Look at the code snippet 4 and 5 which deals with key events in the text fields.

The source code comments are quite self explanatory.

Code Snippet 4 of /src/com/examples/tipcalc/Tipster.java

/*
 * Attach a KeyListener to the Tip Amount, No. of People and Other Tip
 * Percentage text fields
 */
txtAmount.setOnKeyListener(mKeyListener);
txtPeople.setOnKeyListener(mKeyListener);
txtTipOther.setOnKeyListener(mKeyListener);

Notice that we create just one listener instead of creating anonymous/inner listeners for each textfield. It is a stylistic choice to write one large listener or multiple smaller listeners, but I always write it in this style if the listeners are going to perform some common actions. Here the common concern for all the text fields is that they should not be empty, and only when they have values should the Calculate button be enabled.

/*
 * KeyListener for the Total Amount, No of People and Other Tip Percentage
 * fields. We need to apply this key listener to check for following
 * conditions:
 *
 * 1) If user selects Other tip percentage, then the other tip text field
 * should have a valid tip percentage entered by the user. Enable the
 * Calculate button only when user enters a valid value.
 *
 * 2) If user does not enter values in the Total Amount and No of People,
 * we cannot perform the calculations. Hence enable the Calculate button
 * only when user enters a valid values.
 */
private OnKeyListener mKeyListener = new OnKeyListener() {
    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
 
    switch (v.getId()) {
    case R.id.txtAmount:
    case R.id.txtPeople:
        btnCalculate.setEnabled(txtAmount.getText().length() > 0
                && txtPeople.getText().length() > 0);
        break;
    case R.id.txtTipOther:
        btnCalculate.setEnabled(txtAmount.getText().length() > 0
                && txtPeople.getText().length() > 0
                && txtTipOther.getText().length() > 0);
        break;
    }
    return false;
    }
 
};

In line 18 of code snippet 5, we examine the ID of the View. Remember that each widget has a unique ID as we define it in the main,xml file. These values are then defined in the generated R.java class.

In line 19 and 20, if the key event occured in the Total Amount or No. of People fields then we check for the value entered in these fields. We are ensuring that the user has not left the fields blank.

In line 24 we check if the user has selected Other radio button, then we ensure that the Other text field is not empty. We also check once again if the Total Amount and No. of People fields are empty.

So the purpose of our KeyListener is now clear - ensure that all text fields are not empty and only then enable the Calculate button.

Listening to Button clicks Next we look at the 'Calculate' and Reset buttons. When the user clicks these buttons, we use the static interface OnClickListener() which will let us know when a button is clicked.

As we did with text fields, just one listener is created and within it we detect which button was clicked. Depending on the button clicked, the calculate() and reset() methods are called.

Code snippet 6 shows how the click listener is added to the buttons.

Code Snippet 6 of /src/com/examples/tipcalc/Tipster.java /* Attach listener to the Calculate and Reset buttons */ btnCalculate.setOnClickListener(mClickListener); btnReset.setOnClickListener(mClickListener);

Code snippet 7 shows how to detect which button is clicked by checking for the ID of the View that receives the click event.

Code Snippet 7 of /src/com/examples/tipcalc/Tipster.java

/**
 * ClickListener for the Calculate and Reset buttons.
 * Depending on the button clicked, the corresponding
 * method is called.
 */
private OnClickListener mClickListener = new OnClickListener() {
 
    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btnCalculate) {
            calculate();
        } else {
            reset();
        }
    }
};

Resetting the application When the user clicks the Reset button, the text fields should be cleared, the default 15% radio butto should be seleced and any results calculated should be cleared.

Code snippet 8 shows the reset() method.

Code Snippet 8 of /src/com/examples/tipcalc/Tipster.java

/**
 * Resets the results text views at the bottom of the screen as well as
 * resets the text fields and radio buttons.
 */
private void reset() {
    txtTipAmount.setText("");
    txtTotalToPay.setText("");
    txtTipPerPerson.setText("");
    txtAmount.setText("");
    txtPeople.setText("");
    txtTipOther.setText("");
    rdoGroupTips.clearCheck();
    rdoGroupTips.check(R.id.radioFifteen);
    // set focus on the first field
    txtAmount.requestFocus();
}

Validating the input to calculate the tip As I said before, we are limiting what type of values the user can enter in the text fields. However, the user could still enter a value of zero in the Total Amount, No. of People and Other Tip Percentage text fields, thus causing error conditions like divide by zero in our tip calculations.

If the user enters zero then we must show an alert popup asking the user to enter non-zero values. We handle this with a method called showErrorAlert(String errorMessage, final int fieldId), but more about it later.

First, look at code snippet 9 which shows the calculate() method. Notice how the values entered by the user are parsed as Double values.

Checking for zero values Notice line 11 and 16 where we check for zero values. If the user enters zero, then we show an alert popup to warn the user. Next look at line 37, where the Other Tip Percentage text field is enabled because the user selected the Other radio button. Here too, we must check for the tip percentage being zero.

State of radio buttons When the application loads, the 15% radio button is selected by default. If the user changes the selection, then we saw in code snippet 3, in the OnCheckedChangeListener, that we assign the ID of the selected radio button to the member variable radioCheckedId.

But if the user accepts the default selection, then the radioCheckedId will have the default value of -1. In short, we will never know which radio button was selected. Offcourse, we know which one is selected by default and could have coded the logic slightly differently, to assume 15% if radioCheckedId has the value -1. But if you refer the API, you will see that we can call the method getCheckedRadioButtonId() on the RadioGroup and not on individual radio buttons. This is because the OnCheckedChangeListener readily provides us with the ID of the radio button selected.

Showing the results Calculating the tip is simple. If there are no validation errors, the boolean flag isError will be false. Look at lines 49 to 51 in code snippet 9 for the simple tip calculations. Next, the calculated values are set to the TextView widgets from line 53 to 55.

Code Snippet 9 of /src/com/examples/tipcalc/Tipster.java

/**
 * Calculate the tip as per data entered by the user.
 */
private void calculate() {
    Double billAmount = Double.parseDouble(
        txtAmount.getText().toString());
    Double totalPeople = Double.parseDouble(
        txtPeople.getText().toString());
    Double percentage = null;
    boolean isError = false;
    if (billAmount < 1.0) {
        showErrorAlert("Enter a valid Total Amount.",
            txtAmount.getId());
        isError = true;
    }
 
    if (totalPeople < 1.0) {
        showErrorAlert("Enter a valid value for No. of people.",
            txtPeople.getId());
        isError = true;
    }
 
    /*
     * If user never changes radio selection, then it means
     * the default selection of 15% is in effect. But its
     * safer to verify
     */
    if (radioCheckedId == -1) {
        radioCheckedId = rdoGroupTips.getCheckedRadioButtonId();
    }
    if (radioCheckedId == R.id.radioFifteen) {
        percentage = 15.00;
    } else if (radioCheckedId == R.id.radioTwenty) {
        percentage = 20.00;
    } else if (radioCheckedId == R.id.radioOther) {
        percentage = Double.parseDouble(
            txtTipOther.getText().toString());
        if (percentage < 1.0) {
            showErrorAlert("Enter a valid Tip percentage",
                txtTipOther.getId());
            isError = true;
        }
    }
    /*
     * If all fields are populated with valid values, then proceed to
     * calculate the tips
     */
    if (!isError) {
        Double tipAmount = ((billAmount * percentage) / 100);
        Double totalToPay = billAmount + tipAmount;
        Double perPersonPays = totalToPay / totalPeople;
 
        txtTipAmount.setText(tipAmount.toString());
        txtTotalToPay.setText(totalToPay.toString());
        txtTipPerPerson.setText(perPersonPays.toString());
    }
}

Showing the alerts Android provides the AlertDialog class to show alert popups. This lets us show a dialog with upto three buttons and a message.

Code snippet 10 shows the showErrorAlert method which uses this AlertDialog to show the error messages. Notice that we pass two arguments to this method - String errorMessage and int fieldId. The first argument is the error message we want to show to the user. The fieldId is the ID of the field which caused the error condition. After the user dismissed the alert dialog, this fieldID will allow us to request the focus on that field, so the user knows which field has the error.

Code Snippet 10 of /src/com/examples/tipcalc/Tipster.java

/**
 * Shows the error message in an alert dialog
 *
 * @param errorMessage
 *            String the error message to show
 * @param fieldId
 *            the Id of the field which caused the error.
 *            This is required so that the focus can be
 *            set on that field once the dialog is
 *            dismissed.
 */
private void showErrorAlert(String errorMessage,
    final int fieldId) {
    new AlertDialog.Builder(this).setTitle("Error")
    .setMessage(errorMessage).setNeutralButton("Close",
            new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog,
                        int which) {
                    findViewById(fieldId).requestFocus();
                }
            }).show();
}

When all is said and done, it should look like this:

Conclusion Developing for the Android OS is not so different than developing for any other UI toolkit like Microsoft Windows, X Windows, Java Swing, Adobe Flex, etc. Of course Android has its differences and overall a very good design. The XML layout paradigm is quite cool and useful to build complex UIs using simple XML. The event handling model is simple, feature rich and intuitive to use in code.

Download:

The source code for this project is in the Android Cookbook repository at http://github.com/IanDarwin/Android-Cookbook-Examples, in the subdirectory Tipster.
No records found.