RSS

Search Engine

Friday, May 21, 2010

Tipster - Building a tip calculator for the Android OS

Tipster: Introduction

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. I have seen this application on my simple cell phone too. So I thought of implementing it in Android as a means to learn how it all works.

When I looked at the various tutorials, each one demonstrated a specific set of features. I tried different tutorials and then set about to write an application which would be as close to a real world application. Obviously this meant using different features of the API. The end result was a good enough application which used lots of features all in one application.

I know that many developers want a short tutorial with just the code pieces and brief explanations. Somehow, I cannot just post code and few comments. I always end up writing such tutorials as if I am speaking out to a live person.

So here it is, whatever I learnt and applied, for your perusal.

Building Blocks

I am assuming that you have read the Google Android website and know a bit about Android applications. At least enough to know how to build and run the Hello World example. It would be best if you read through this set of API examples.

So, lets proceed.

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.

Creating the project using the Android Eclipse Plugin

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.

project layout

Fig. 1 - Project layout for Tipster in Eclipse

Creating the Layout and placing the Widgets

The end goal is to create a layout as shown in the following screen shot.

screen layout

Fig. 2 - The Tipster screen layout.

Widgets and layouts:

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.

I came across a UI tool Droid Draw, that lets you create a layout by drag-and-drop of widgets from a palette, just like any form designer tool. However, I would 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

This file 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.

Open code in a window

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 -

Open code in a window

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.

Open code in a window

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. Click here to go to main.xml and then look at line 66, 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.

Open code in a window

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.

Open code in a window

Notice that we create just one listener instead of creating anonymous/inner listeners for each textfield. I am not sure if my style is better or recommended, 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.

Open code in a window

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.

Open code in a window

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

Open code in a window

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.

Open code in a window

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.

Open code in a window

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.

Open code in a window

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. Offcourse 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.

I am enjoying delving into the Android OS and learning new things every time I dive in. I look forward to being able to deploy an application on a real Google Phone :)

Resources

In this tipster.zip file, you will find the necessary Eclipse project and the source files. Download the zip file and extract it to a local folder. From Eclipse, simply use the File-->Import menu option to import the project.

Happy coding! :)

0 comments:

Post a Comment