In the first part of this tutorial series, we built a simple brew timer application using Android and Eclipse. In this second part, we’ll continue developing the application by adding extra functionality. In doing this, you’ll be introduced to some important and powerful features of the Android SDK, including Persistent data storage, Activities and Intent as well as Shared user preferences.
To follow this tutorial, you’ll need the code from the previous article. If you want to get started right away, grab the code from GitHub and check out the tutorial_part_1 tag using this:
$ git clone git://github.com/cblunt/BrewClock.git $ cd BrewClock $ git checkout tutorial_part_1
Once you’ve checked out the code on GitHub, you’ll need to import the project into Eclipse:
- Launch Eclipse and choose File → Import…
- In the Import window, select “Existing Projects into Workspace� and click “Next.�
- On the next screen, click “Browse,� and select the project folder that you cloned from GitHub.
- Click “Finish� to import your project into Eclipse.
After importing the project into Eclipse, you might receive a warning message:
Android required .class compatibility set to 5.0. Please fix project properties.
If this is the case, right-click on the newly imported “BrewClock� project in the “Project Explorer,� choose “Fix Project Properties,� and then restart Eclipse.
Getting Started With Data Storage
Currently, BrewClock lets users set a specific time for brewing their favorite cups of tea. This is great, but what if they regularly drink a variety of teas, each with their own different brewing times? At the moment, users have to remember brewing times for all their favorite teas! This doesn’t make for a great user experience. So, in this tutorial we’ll develop functionality to let users store brewing times for their favorite teas and then choose from that list of teas when they make a brew.
To do this, we’ll take advantage of Android’s rich data-storage API. Android offers several ways to store data, two of which we’ll cover in this article. The first, more powerful option, uses the SQLite database engine to store data for our application.
SQLite is a popular and lightweight SQL database engine that saves data in a single file. It is often used in desktop and embedded applications, where running a client-server SQL engine (such as MySQL or PostgreSQL) isn’t feasible.
Every application installed on an Android device can save and use any number of SQLite database files (subject to storage capacity), which the system will manage automatically. An application’s databases are private and so cannot be accessed by any other applications. (Data can be shared through the ContentProvider
class, but we won’t cover content providers in this tutorial.) Database files persist when the application is upgraded and are deleted when the application is uninstalled.
We’ll use a simple SQLite database in BrewClock to maintain a list of teas and their appropriate brewing times. Here’s an overview of how our database schema will look:
+-------------------------------------+ | Table: teas | +------------+------------------------+ | Column | Description | +------------+------------------------+ | _ID | integer, autoincrement | | name | text, not null | | brew_time | integer, not null | +------------+------------------------+
If you’ve worked with SQL before, this should look fairly familiar. The database table has three columns: a unique identifier (_ID
), name and brewing time. We’ll use the APIs provided by Android to create the database table in our code. The system will take care of creating the database file in the right location for our application.
Abstracting the Database
To ensure the database code is easy to maintain, we’ll abstract all the code for handling database creation, inserts and queries into a separate class, TeaData
. This should be fairly familiar if you’re used to the model-view-controller approach. All the database code is kept in a separate class from our BrewClockActivity
. The Activity can then just instantiate a new TeaData
instance (which will connect to the database) and do what it needs to do. Working in this way enables us to easily change the database in one place without having to change anything in any other parts of our application that deal with the database.
Create a new class called TeaData
in the BrewClock project by going to File → New → Class. Ensure that TeaData
extends the android.database.sqlite.SQLiteOpenHelper
class and that you check the box for “Constructors from superclass.�
The TeaData
class will automatically handle the creation and versioning of a SQLite database for your application. We’ll also add methods to give other parts of our code an interface to the database.
Add two constants to TeaData
to store the name and version of the database, the table’s name and the names of columns in that table. We’ll use the Android-provided constant BaseColumns._ID
for the table’s unique id column:
// src/com/example/brewclock/TeaData.java import android.app.Activity; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.DatabaseUtils; import android.provider.BaseColumns; public class TeaData extends SQLiteOpenHelper { private static final String DATABASE_NAME = "teas.db"; private static final int DATABASE_VERSION = 1; public static final String TABLE_NAME = "teas"; public static final String _ID = BaseColumns._ID; public static final String NAME = "name"; public static final String BREW_TIME = "brew_time"; // … }
Add a constructor to TeaData
that calls its parent method, supplying our database name and version. Android will automatically handle opening the database (and creating it if it does not exist).
// src/com/example/brewclock/TeaData.java public TeaData(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); }
We’ll need to override the onCreate
method to execute a string of SQL commands that create the database table for our tea. Android will handle this method for us, calling onCreate
when the database file is first created.
On subsequent launches, Android checks the version of the database against the DATABASE_VERSION
number we supplied to the constructor. If the version has changed, Android will call the onUpgrade
method, which is where you would write any code to modify the database structure. In this tutorial, we’ll just ask Android to drop and recreate the database.
So, add the following code to onCreate
:
// src/com/example/brewclock/TeaData.java @Override public void onCreate(SQLiteDatabase db) { // CREATE TABLE teas (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, brew_time INTEGER); String sql = "CREATE TABLE " + TABLE_NAME + " (" + _ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + NAME + " TEXT NOT NULL, " + BREW_TIME + " INTEGER" + ");"; db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); onCreate(db); }
Next, we’ll add a new method to TeaData
that lets us easily add new tea records to the database. We’ll supply the method with a name and brewing time for the tea to be added. Rather than forcing us to write out the raw SQL to do this, Android supplies a set of classes for inserting records into the database. First, we create a set of ContentValues
, pushing the relevant values into that set.
With an instance of ContentValues
, we simply supply the column name and the value to insert. Android takes care of creating and running the appropriate SQL. Using Android’s database classes ensures that the writes are safe, and if the data storage mechanism changes in a future Android release, our code will still work.
Add a new method, insert()
, to the TeaData
class:
// src/com/example/brewclock/TeaData.java public void insert(String name, int brewTime) { SQLiteDatabase db = getWritableDatabase(); ContentValues values = new ContentValues(); values.put(NAME, name); values.put(BREW_TIME, brewTime); db.insertOrThrow(TABLE_NAME, null, values); }
Retrieving Data
With the ability to save data into the database, we’ll also need a way to get it back out. Android provides the Cursor
interface for doing just this. A Cursor
represents the results of running a SQL query against the database, and it maintains a pointer to one row within that result set. This pointer can be moved forwards and backwards through the results, returning the values from each column. It can help to visualize this:
SQL Query: SELECT * from teas LIMIT 3; +-----------------------------------+ | _ID | name | brew_time | +-----------------------------------+ | 1 | Earl Grey | 3 | | 2 | Green | 1 | <= Cursor | 3 | Assam | 5 | +-------+-------------+-------------+
In this example, the Cursor is pointing at the second row in the result set (Green tea). We could move the Cursor back a row to represent the first row (Earl Grey) by calling cursor.moveToPrevious()
, or move forward to the Assam row with moveToNext()
. To fetch the name of the tea that the Cursor is pointing out, we would call cursor.getString(1)
, where 1
is the column index of the column we wish to retrieve (note that the index is zero-based, so column 0 is the first column, 1 the second column and so on).
Now that you know about Cursors, add a method that creates a Cursor
object that returns all the teas in our database. Add an all
method to TeaData
:
// src/com/example/brewclock/TeaData.java public Cursor all(Activity activity) { String[] from = { _ID, NAME, BREW_TIME }; String order = NAME; SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.query(TABLE_NAME, from, null, null, null, null, order); activity.startManagingCursor(cursor); return cursor; }
Let’s go over this method in detail, because it looks a little strange at first. Again, rather than writing raw SQL to query the database, we make use of Android’s database interface methods.
First, we need to tell Android which columns from our database we’re interested in. To do this, we create an array of strings—each one of the column identifiers that we defined at the top of TeaData
. We’ll also set the column that we want to order the results by and store it in the order
string.
Next, we create a read-only connection to the database using getReadableDatabase()
, and with that connection, we tell Android to run a query using the query()
method. The query()
method takes a set of parameters that Android internally converts into a SQL query. Again, Android’s abstraction layer ensures that our application code will likely continue to work, even if the underlying data storage changes in a future version of Android.
Because we just want to return every tea in the database, we don’t apply any joins, filters or groups (i.e. WHERE
, JOIN
, and GROUP BY
clauses in SQL) to the method. The from
and order
variables tell the query what columns to return on the database and the order in which they are retrieved. We use the SQLiteDatabase.query()
method as an interface to the database.
Last, we ask the supplied Activity (in this case, our BrewClockActivity
) to manage the Cursor. Usually, a Cursor must be manually refreshed to reload any new data, so if we added a new tea to our database, we would have to remember to refresh our Cursor. Instead, Android can take care of this for us, recreating the results whenever the Activity is suspended and resumed, by calling startManagingCursor()
.
Finally, we’ll add another utility method to return the number of records in the table. Once again, Android provides a handy utility to do this for us in the DatabaseUtils
class:
Add the following method, count
, to your TeaData
class:
// src/com/example/brewclock/TeaData.java public long count() { SQLiteDatabase db = getReadableDatabase(); return DatabaseUtils.queryNumEntries(db, TABLE_NAME); }
Save the TeaData
class, and fix any missing imports using Eclipse (Source → Organize Imports). With our data class finished, it’s time to change BrewClock’s interface to make use of the database!
Modify BrewClock’s Interface to Allow Tea Selection
The purpose of storing preset teas and brew times is to let the user quickly select their favorite tea from the presets. To facilitate this, we’ll add a Spinner
(analogous to a pop-up menu in desktop interfaces) to the main BrewClock interface, populated with the list of teas from TeaData
.
As in the previous tutorial, use Eclipse’s layout editor to add the Spinner to BrewClock’s main interface layout XML file. Add the following code just below the LinearLayout
for the brew count label (around line 24). Remember, you can switch to the “Code View� tab along the bottom of the window if Eclipse opens the visual layout editor.
<!-- /res/layout/main.xml --> <!-- Tea Selection --> <LinearLayout android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content"> <Spinner android:id="@+id/tea_spinner" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>
In the BrewClockActivity
class, add a member variable to reference the Spinner, and connect it to the interface using findViewById
:
// src/com/example/brewclock/BrewClockActivity.java protected Spinner teaSpinner; protected TeaData teaData; // … public void onCreate(Bundle savedInstanceState) { // … teaData = new TeaData(this); teaSpinner = (Spinner) findViewById(R.id.tea_spinner); }
Try running your application to make sure the new interface works correctly. You should see a blank pop-up menu (or Spinner) just below the brew count. If you tap the spinner, Android handles displaying a pop-up menu so that you can choose an option for the spinner. At the moment, the menu is empty, so we’ll remedy that by binding the Spinner to our tea database.
Data Binding
When Android retrieves data from a database, it returns a Cursor
object. The Cursor represents a set of results from the database and can be moved through the results to retrieve values. We can easily bind these results to a view (in this case, the Spinner) using a set of classes provided by Android called “Adapters.� Adapters do all the hard work of fetching database results from a Cursor
and displaying them in the interface.
Remember that our TeaData.all()
method already returns a Cursor populated with the contents of our teas table. Using that Cursor, all we need to do is create a SimpleCursorAdapter
to bind its data to our teaSpinner
, and Android will take care of populating the spinner’s options.
Connect the Cursor returned by teaData.all()
to the Spinner by creating a SimpleCursorAdapter
:
// com/example/brewclock/BrewClockActivity.java public void onCreate(Bundle savedInstanceState) { // … Cursor cursor = teaData.all(this); SimpleCursorAdapter teaCursorAdapter = new SimpleCursorAdapter( this, android.R.layout.simple_spinner_item, cursor, new String[] { TeaData.NAME }, new int[] { android.R.id.text1 } ); teaSpinner.setAdapter(teaCursorAdapter); teaCursorAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); }
Notice that we’ve made use of Android’s built-in android.R
object. This provides some generic default resources for your application, such as simple views and layouts. In this case, we’ve used android.R.layout.simple_spinner_item
, which is a simple text label layout.
If you run the application again, you’ll see that the spinner is still empty! Even though we’ve connected the spinner to our database, there are no records in the database to display.
Let’s give the user a choice of teas by adding some default records to the database in BrewClock’s constructor. To avoid duplicate entries, we’ll add only the default teas if the database is empty. We can make use of TeaData’s count()
method to check if this is the case.
Add code to create a default set of teas if the database is empty. Add this line just above the code to fetch the teas from teaData
:
// com/example/brewclock/BrewClockActivity.java public void onCreate(Bundle savedInstanceState) { // … // Add some default tea data! (Adjust to your preference :) if(teaData.count() == 0) { teaData.insert("Earl Grey", 3); teaData.insert("Assam", 3); teaData.insert("Jasmine Green", 1); teaData.insert("Darjeeling", 2); } // Code from the previous step: Cursor cursor = teaData.all(this); // … }
Now run the application again. You’ll now see that your tea Spinner has the first tea selected. Tapping on the Spinner lets you select one of the teas from your database!
Congratulations! You’ve successfully connected your interface to a data source. This is one of the most important aspects of any software application. As you’ve seen, Android makes this task fairly easy, but it is extremely powerful. Using cursors and adapters, you can take virtually any data source (from a simple array of strings to a complex relational database query) and bind it to any type of view: a spinner, a list view or even an iTunes-like cover-flow gallery!
Although now would be a good time for a brew, our work isn’t over yet. While you can choose different teas from the Spinner, making a selection doesn’t do anything. We need to find out which tea the user has selected and update the brew time accordingly.
Read Selected Tea, and Update Brew Time
To determine which tea the user has selected from our database, BrewClockActivity
needs to listen for an event. Similar to the OnClickListener
event that is triggered by button presses, we’ll implement the OnItemSelectedListener
. Events in this listener are triggered when the user makes a selection from a view, such as our Spinner.
Enable the onItemSelectedListener
in BrewClockActivity
by adding it to the class declaration. Remember to implement the interface methods onItemSelected()
and onNothingSelected()
:
// src/com/example/brewclock/BrewClockActivity.java public class BrewClockActivity extends Activity implements OnClickListener, OnItemSelectedListener { // … public void onItemSelected(AdapterView<?> spinner, View view, int position, long id) { if(spinner == teaSpinner) { // Update the brew time with the selected tea’s brewtime Cursor cursor = (Cursor) spinner.getSelectedItem(); setBrewTime(cursor.getInt(2)); } } public void onNothingSelected(AdapterView<?> adapterView) { // Do nothing } }
Here we check whether the spinner that triggered the onItemSelected
event was BrewClock’s teaSpinner
. If so, we retrieve a Cursor object that represents the selected record. This is all handled for us by the SimpleCursorAdapter
that connects teaData
to the Spinner. Android knows which query populates the Spinner and which item the user has selected. It uses these to return the single row from the database, representing the user’s selected tea.
Cursor’s getInt()
method takes the index of the column we want to retrieve. Remember that when we built our Cursor in teaData.all()
, the columns we read were _ID
, NAME
and BREW_TIME
. Assuming we chose Jasmine tea in teaSpinner
, the Cursor returned by our selection would be pointing at that record in the database.
We then ask the Cursor to retrieve the value from column 2 (using getInt(2)
), which in this query is our BREW_TIME
column. This value is supplied to our existing setBrewTime()
method, which updates the interface to show the selected tea’s brewing time.
Finally, we need to tell the teaSpinner
that BrewClockActivity
is listening for OnItemSelected
events. Add the following line to BrewClockActivity
’s onCreate
method:
// src/com/example/brewclock/BrewClockActivity.java public void onCreate() { // … teaSpinner.setOnItemSelectedListener(this); }
That should do it! Run your application again, and try selecting different teas from the Spinner. Each time you select a tea, its brew time will be shown on the countdown clock. The rest of our code already handles counting down from the current brew time, so we now have a fully working brew timer, with a list of preset teas.
You can, of course, go back into the code and add more preset teas to the database to suit your tastes. But what if we released BrewClock to the market? Every time someone wanted to add a new tea to the database, we’d need to manually update the database, and republish the application; everyone would need to update, and everybody would have the same list of teas. That sounds pretty inflexible, and a lot of work for us!
It would be much better if the user had some way to add their own teas and preferences to the database. We’ll tackle that next…
Introducing Activities
Each screen in your application and its associated code is an Activity
. Every time you go from one screen to another, Android creates a new Activity. In reality, although an application may comprise any number of screens/activities, Android treats them as separate entities. Activities work together to form a cohesive experience because Android lets you easily pass data between them.
In this final section, you’ll add a new Activity (AddTeaActivity
) to your application and register it with the Android system. You’ll then pass data from the original BrewClockActivity
to this new Activity.
First, though, we need to give the user a way to switch to the new Activity. We’ll do this using an options menu.
Options Menus
Options menus are the pop-up menus that appear when the user hits the “Menu� key on their device. Android handles the creation and display of options menus automatically; you just need to tell it what options to display and what to do when an option is chosen by the user.
However, rather than hard-coding our labels into the menu itself, we’ll make use of Android string resources. String resources let you maintain all the human-readable strings and labels for your application in one file, calling them within your code. This means there’s only one place in your code where you need to change strings in the future.
In the project explorer, navigate to “res/values� and you will see that a strings.xml file already exists. This was created by Eclipse when we first created the project, and it is used to store any strings of text that we want to use throughout the application.
Open strings.xml by double clicking on it, and switch to the XML view by clicking the strings.xml tab along the bottom of the window.
Add the following line within the <resources>…</resources>
element:
<!-- res/values/strings.xml --> <resources> <!-- … --> <string name="add_tea_label">Add Tea</string> </resources>
Here you’ve defined a string, add_tea_label
, and its associated text. We can use add_tea_label
to reference the string throughout the application’s code. If the label needs to change for some reason in the future, you’ll need to change it only once in this file.
Next, let’s create a new file to define our options menu. Just like strings and layouts, menus are defined in an XML file, so we’ll start by creating a new XML file in Eclipse:
Create a new Android XML file in Eclipse by choosing File → New → Other, and then select “Android XML File.â€�
Select a resource type of “Menu,� and save the file as main.xml. Eclipse will automatically create a folder, res/menu, where your menu XML files will be stored.
Open the res/menus/main.xml file, and switch to XML view by clicking the “main.xml� tab along the bottom of the window.
Add a new menu item, add_tea
.
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/add_tea" android:title="@string/add_tea_label" /> </menu>
Notice the android:title
attribute is set to @string/add_tea_label
. This tells Android to look up add_tea_label
in our strings.xml file and return the associated label. In this case, our menu item will have a label “Add Tea.�
Next, we’ll tell our Activity to display the options menu when the user hits the “Menu� key on their device.
Back in BrewClockActivity.java, override the onCreateOptionsMenu
method to tell Android to load our menu when the user presses the “Menu� button:
// src/com/example/brewclock/BrewClockActivity.java @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main, menu); return true; }
When the user presses the “Menu� button on their device, Android will now call onCreateOptionsMenu
. In this method, we create a MenuInflater
, which loads a menu resource from your application’s package. Just like the buttons and text fields that make up your application’s layout, the main.xml resource is available via the global R
object, so we use that to supply the MenuInflater
with our menu resource.
To test the menu, save and run the application in the Android emulator. While it’s running, press the “Menu� button, and you’ll see the options menu pop up with an “Add Tea� option.
If you tap the “Add Tea� option, Android automatically detects the tap and closes the menu. In the background, Android will notify the application that the option was tapped.
Handling Menu Taps
When the user taps the “Add Teaâ€� menu option, we want to display a new Activity so that they can enter the details of the tea to be added. Start by creating that new Activity by selecting File → New → Class.
Name the new class AddTeaActivity
, and make sure it inherits from the android.app.Activity
class. It should also be in the com.example.brewclock
package:
// src/com/example/brewclock/AddTeaActivity.java package com.example.brewclock; import android.app.Activity; import android.os.Bundle; public class AddTeaActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } }
This simple, blank Activity won’t do anything yet, but it gives us enough to finish our options menu.
Add the onOptionsItemSelected
override method to BrewClockActivity
. This is the method that Android calls when you tap on a MenuItem
(notice it receives the tapped MenuItem
in the item parameter):
// src/com/example/brewclock/BrewClockActivity.java @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case R.id.add_tea: Intent intent = new Intent(this, AddTeaActivity.class); startActivity(intent); return true; default: return super.onOptionsItemSelected(item); } }
With this code, we’ve told Android that when the “Add Tea� menu item is tapped, we want to start a new Activity; in this case, AddTeaActivity
. However, rather than directly creating an instance of AddTeaActivity
, notice that we’ve used an Intent
. Intents are a powerful feature of the Android framework: they bind Activities together to make up an application and allow data to be passed between them.
Intents even let your application take advantage of any Activities within other applications that the user has installed. For example, when the user asks to display a picture from a gallery, Android automatically displays a dialogue to the user allowing them to pick the application that displays the image. Any applications that are registered to handle image display will be shown in the dialogue.
Intents are a powerful and complex topic, so it’s worth reading about them in detail in the official Android SDK documentation.
Let’s try running our application to test out the new “Add Tea� screen.
Run your project, tap the “Menu� button and then tap “Add Tea.�
Instead of seeing your “Add Tea� Activity as expected, you’ll be presented with a dialogue that is all too common for Android developers:
Although we created the Intent
and told it to start our AddTeaActivity
Activity, the application crashed because we haven’t yet registered it within Android. The system doesn’t know where to find the Activity we’re trying to run (remember that Intents can start Activities from any application installed on the device). Let’s remedy this by registering our Activity within the application manifest file.
Open your application’s manifest file, AndroidManifest.xml in Eclipse, and switch to the code view by selecting the “AndroidManifest.xml� tab along the bottom of the window.
The application’s manifest file is where you define global settings and information about your application. You’ll see that it already declares .BrewClockActivity
as the Activity to run when the application is launched.
Within <application>
, add a new <activity>
node to describe the “Add Tea� Activity. Use the same add_tea_label
string that we declared earlier in strings.xml for the Activity’s title:
<!-- AndroidManifest.xml --> <application …> … <activity android:name=".AddTeaActivity" android:label="@string/add_tea_label" /> </application>
Save the manifest file before running BrewClock again. This time, when you open the menu and tap “Add Tea,� Android will start the AddTeaActivity
. Hit the “Back� button to go back to the main screen.
With the Activities hooked together, it’s time to build an interface for adding tea!
Building The Tea Editor Interface
Building the interface to add a tea is very similar to how we built the main BrewClock interface in the previous tutorial. Start by creating a new layout file, and then add the appropriate XML, as below.
Alternatively, you could use Android’s recently improved layout editor in Eclipse to build a suitable interface. Create a new XML file in which to define the layout. Go to File → New, then select “Android XML File,â€� and select a “Layoutâ€� type. Name the file add_tea.xml.
Replace the contents of add_tea.xml with the following layout:
<!-- res/layouts/add_tea.xml --> <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" android:padding="10dip"> <TextView android:text="@string/tea_name_label" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <EditText android:id="@+id/tea_name" android:layout_width="fill_parent" android:layout_height="wrap_content"/> <TextView android:text="@string/brew_time_label" android:layout_width="wrap_content" android:layout_height="wrap_content"/> <SeekBar android:id="@+id/brew_time_seekbar" android:layout_width="fill_parent" android:layout_height="wrap_content" android:progress="2" android:max="9" /> <TextView android:id="@+id/brew_time_value" android:text="3 m" android:textSize="20dip" android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" /> </LinearLayout>
We’ll also need to add some new strings to strings.xml for the labels used in this interface:
<!-- res/values/strings.xml --> <resources> <!-- … --> <string name="tea_name_label">Tea Name</string> <string name="brew_time_label">Brew Time</string> </resources>
In this layout, we’ve added a new type of interface widget, the SeekBar. This lets the user easily specify a brew time by dragging a thumb from left to right. The range of values that the SeekBar produces always runs from zero (0) to the value of android:max
.
In this interface, we’ve used a scale of 0 to 9, which we will map to brew times of 1 to 10 minutes (brewing for 0 minutes would be a waste of good tea!). First, though, we need to make sure that AddTeaActivity
loads our new interface:
Add the following line of code to the Activity’s onCreate()
method that loads and displays the add_tea
layout file:
// src/com/example/brewclock/AddTeaActivity.java public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.add_tea); }
Now test your application by running it, pressing the “Menu� button and tapping “Add Tea� from the menu.
You’ll see your new interface on the “Add Tea� screen. You can enter text and slide the SeekBar left and right. But as you’d expect, nothing works yet because we haven’t hooked up any code.
Declare some properties in AddTeaActivity
to reference our interface elements:
// src/com/example/brewclock/AddTeaActivity.java public class AddTeaActivity { // … /** Properties **/ protected EditText teaName; protected SeekBar brewTimeSeekBar; protected TextView brewTimeLabel; // …
Next, connect those properties to your interface:
public void onCreate(Bundle savedInstanceState) { // … // Connect interface elements to properties teaName = (EditText) findViewById(R.id.tea_name); brewTimeSeekBar = (SeekBar) findViewById(R.id.brew_time_seekbar); brewTimeLabel = (TextView) findViewById(R.id.brew_time_value); }
The interface is fairly simple, and the only events we need to listen for are changes to the SeekBar. When the user moves the SeekBar thumb left or right, our application will need to read the new value and update the label below with the selected brew time. We’ll use a Listener
to detect when the SeekBar is changed:
Add an onSeekBarChangedListener
interface to the AddTeaActivity
class declaration, and add the required methods:
// src/com/example/brewclock/AddTeaActivity.java public class AddTeaActivity extends Activity implements OnSeekBarChangeListener { // … public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // TODO Detect change in progress } public void onStartTrackingTouch(SeekBar seekBar) {} public void onStopTrackingTouch(SeekBar seekBar) {} }
The only event we’re interested in is onProgressChanged
, so we need to add the code below to that method to update the brew time label with the selected value. Remember that our SeekBar values range from 0 to 9, so we’ll add 1 to the supplied value so that it makes more sense to the user:
Add the following code to onProgressChanged()
in AddTeaActivity.java:
// src/com/example/brewclock/AddTeaActivity.java public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if(seekBar == brewTimeSeekBar) { // Update the brew time label with the chosen value. brewTimeLabel.setText((progress + 1) + " m"); } }
Set the SeekBar’s listener to be our AddTeaActivity
in onCreate
:
// src/com/example/brewclock/AddTeaActivity.java public void onCreate(Bundle savedInstanceState) { // … // Setup Listeners brewTimeSeekBar.setOnSeekBarChangeListener(this); }
Now when run the application and slide the SeekBar left to right, the brew time label will be updated with the correct value:
Saving Tea
With a working interface for adding teas, all that’s left is to give the user the option to save their new tea to the database. We’ll also add a little validation to the interface so that the user cannot save an empty tea to the database!
Start by opening strings.xml in the editor and adding some new labels for our application:
<!-- res/values/strings.xml --> <string name="save_tea_label">Save Tea</string> <string name="invalid_tea_title">Tea could not be saved.</string> <string name="invalid_tea_no_name">Enter a name for your tea.</string>
Just like before, we’ll need to create a new options menu for AddTeaActivity
so that the user can save their favorite tea:
Create a new XML file, add_tea.xml, in the res/menus folder by choosing File → New and then Other → Android XML File. Remember to select “Menuâ€� as the resource type.
Add an item to the new menu for saving the tea:
<!-- res/menus/add_tea.xml --> <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:title="@string/save_tea_label" android:id="@+id/save_tea" /> </menu>
Back in AddTeaActivity
, add the override methods for onCreateOptionsMenu
and onOptionsItemSelected
, just like you did in BrewClockActivity
. However, this time, you’ll supply the add_tea.xml resource file to the MenuInflater
:
// src/com/example/brewclock/AddTeaActivity.java @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.add_tea, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch(item.getItemId()) { case R.id.save_tea: saveTea(); default: return super.onOptionsItemSelected(item); } }
Next, we’ll add a new method, saveTea()
, to handle saving the tea. The saveTea
method first reads the name and brew time values chosen by the user, validates them and (if all is well) saves them to the database:
// src/com/example/brewclock/AddTeaActivity.java public boolean saveTea() { // Read values from the interface String teaNameText = teaName.getText().toString(); int brewTimeValue = brewTimeSeekBar.getProgress() + 1; // Validate a name has been entered for the tea if(teaNameText.length() < 2) { AlertDialog.Builder dialog = new AlertDialog.Builder(this); dialog.setTitle(R.string.invalid_tea_title); dialog.setMessage(R.string.invalid_tea_no_name); dialog.show(); return false; } // The tea is valid, so connect to the tea database and insert the tea TeaData teaData = new TeaData(this); teaData.insert(teaNameText, brewTimeValue); teaData.close(); return true; }
This is quite a hefty chunk of code, so let’s go over the logic.
First, we read the values of the EditText teaName
and the SeekBar brewTimeSeekBar
(remembering to add 1 to the value to ensure a brew time of between 1 and 10 minutes). Next, we validate that a name has been entered that is two or more characters (this is really simple validation; you might want to experiment doing something more elaborate, such as using regular expressions).
If the tea name is not valid, we need to let the user know. We make use of one of Android’s helper classes, AlertDialog.Builder
, which gives us a handy shortcut for creating and displaying a modal dialog window. After setting the title and error message (using our string resources), the dialogue is displayed by calling its show()
method. This dialogue is modal, so the user will have to dismiss it by pressing the “Back� key. At this point, we don’t want to save any data, so just return false
out of the method.
If the tea is valid, we create a new temporary connection to our tea database using the TeaData
class. This demonstrates the advantage of abstracting your database access to a separate class: you can access it from anywhere in the application!
After calling teaData.insert()
to add our tea to the database, we no longer need this database connection, so we close it before returning true
to indicate that the save was successful.
Try this out by running the application in the emulator, pressing “Menu� and tapping “Add Tea.� Once on the “Add Tea� screen, try saving an empty tea by pressing “Menu� again and tapping “Save Tea.� With your validation in place, you’ll be presented with an error message:
Next, try entering a name for your tea, choosing a suitable brew time, and choosing “Save Tea� from the menu again. This time, you won’t see an error message. In fact, you’ll see nothing at all.
Improving the User Experience
While functional, this isn’t a great user experience. The user doesn’t know that their tea has been successfully saved. In fact, the only way to check is to go back from the “Add Tea� Activity and check the list of teas. Not great. Letting the user know that their tea was successfully saved would be much better. Let’s show a message on the screen when a tea has been added successfully.
We want the message to be passive, or non-modal, so using an AlertDialog
like before won’t help. Instead, we’ll make use of another popular Android feature, the Toast
.
Toasts display a short message near the bottom of the screen but do not interrupt the user. They’re often used for non-critical notifications and status updates.
Start by adding a new string to the strings.xml resource file. Notice the %s
in the string? We’ll use this in the next step to interpolate the name of the saved tea into the message!
<!-- res/values/strings.xml --> <string name="save_tea_success">%s tea has been saved.</string>
Modify the code in onOptionsItemSelected
to create and show a Toast
pop-up if the result of saveTea()
is true
. The second parameter uses of getString()
interpolate the name of our tea into the Toast
message. Finally, we clear the “Tea Name� text so that the user can quickly add more teas!
// src/com/example/brewclock/AddTeaActivity.java // … switch(item.getItemId()) { case R.id.save_tea: if(saveTea()) { Toast.makeText(this, getString(R.string.save_tea_success, teaName.getText().toString()), Toast.LENGTH_SHORT).show(); teaName.setText(""); } // …
Now re-run your application and try adding and saving a new tea. You’ll see a nice Toast
pop up to let you know the tea has been saved. The getString()
method interpolates the name of the tea that was saved into the XML string, where we placed the %s
.
Click the “Back� button to return to the application’s main screen, and tap the tea spinner. The new teas you added in the database now show up as options in the spinner!
User Preferences
BrewClock is now fully functional. Users can add their favorite teas and the respective brewing times to the database, and they can quickly select them to start a new brew. Any teas added to BrewClock are saved in the database, so even if we quit the application and come back to it later, our list of teas is still available.
One thing you might notice when restarting BrewClock, though, is that the brew counter is reset to 0. This makes keeping track of our daily tea intake (a vital statistic!) difficult. As a final exercise, let’s save the total brew count to the device.
Rather than adding another table to our teas database, we’ll make use of Android’s “Shared Preferences,� a simple database that Android provides to your application for storing simple data (strings, numbers, etc.), such as high scores in games and user preferences.
Start by adding a couple of constants to the top of BrewClockActivity.java. These will store the name of your shared preferences file and the name of the key we’ll use to access the brew count. Android takes care of saving and persisting our shared preferences file.
// src/com/example/brewclock/BrewClockActivity.java protected static final String SHARED_PREFS_NAME = "brew_count_preferences"; protected static final String BREW_COUNT_SHARED_PREF = "brew_count";
Next, we’ll need to make some changes to the code so that we can read and write the brew count to the user preferences, rather than relying on an initial value in our code. In BrewClockActivity
’s onCreate
method, change the lines around setBrewCount(0)
to the following:
// src/com/example/brewclock/BrewClockActivity.java public void onCreate() { // … // Set the initial brew values SharedPreferences sharedPreferences = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE); brewCount = sharedPreferences.getInt(BREW_COUNT_SHARED_PREF, 0); setBrewCount(brewCount); // … }
Here we’re retrieving an instance of the application’s shared preferences using SharedPreferences
, and asking for the value of the brew_count
key (identified by the BREW_COUNT_SHARED_PREF
constant that was declared earlier). If a value is found, it will be returned; if not, we’ll use the default value in the second parameter of getInt
(in this case, 0).
Now that we can retrieve the stored value of brew count, we need to ensure its value is saved to SharedPreferences
whenever the count is updated.
Add the following code to setBrewCount
in BrewClockActivity
:
// src/com/example/brewclock/BrewClockActivity.java public void setBrewCount(int count) { brewCount = count; brewCountLabel.setText(String.valueOf(brewCount)); // Update the brewCount and write the value to the shared preferences. SharedPreferences.Editor editor = getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE).edit(); editor.putInt(BREW_COUNT_SHARED_PREF, brewCount); editor.commit(); }
Shared preferences are never saved directly. Instead, we make use of Android’s SharedPreferences.Editor
class. Calling edit()
on SharedPreferences
returns an editor
instance, which can then be used to set values in our preferences. When the values are ready to be saved to the shared preferences file, we just call commit()
.
With our application’s code all wrapped up, it’s time to test everything!
Run the application on the emulator, and time a few brews (this is the perfect excuse to go and make a well-deserved tea or two!), and then quit the application. Try running another application that is installed on the emulator to ensure BrewClock is terminated. Remember that Android doesn’t terminate an Activity until it starts to run out of memory.
When you next run the application, you’ll see that your previous brew count is maintained, and all your existing teas are saved!
Summary
Congratulations! You’ve built a fully working Android application that makes use of a number of core components of the Android SDK. In this tutorial, you have seen how to:
- Create a simple SQLite database to store your application’s data;
- Make use of Android’s database classes and write a custom class to abstract the data access;
- Add option menus to your application;
- Create and register new Activities within your application and bind them together into a coherent interface using Intents;
- Store and retrieve simple user data and settings using the built-in “Shared Preferences� database.
Data storage and persistence is an important topic, no matter what type of application you’re building. From utilities and business tools to 3-D games, nearly every application will need to make use of the data tools provided by Android.
Activities
BrewClock is now on its way to being a fully functional application. However, we could still implement a few more features to improve the user experience. For example, you might like to develop your skills by trying any of the following:
- Checking for duplicate tea name entries before saving a tea,
- Adding a menu option to reset the brew counter to 0,
- Storing the last-chosen brew time in a shared preference so that the application defaults to that value when restarted,
- Adding an option for the user to delete teas from the database.
Solutions for the Activities will be included in a future branch on the GitHub repository, where you’ll find the full source-code listings. You can download the working tutorial code by switching your copy of the code to the tutorial_2
branch:
# If you’ve not already cloned the repository, # you’ll need to do that first: # $ git clone git://github.com/cblunt/BrewClock.git # $ cd BrewClock $ git checkout tutorial_2
I hope you’ve enjoyed working through this tutorial and that it helps you in designing and building your great Android applications. Please let me know how you get on in the comments below, or feel free to drop me an email.
Thanks to Anselm for his suggestions and feedback!
(al)
© Chris Blunt for Smashing Magazine, 2011. | Permalink | Post a comment | Smashing Shop | Smashing Network | About Us
Post tags: android, development, programming