[Android Software Development] michael.the.don:
[Ultimate Guide][Part 7-released][5/09/2011]Android App Development - From Scratch

PART-VII

Android Techniques for Saving Data

The data persistence techniques in Android provide options for balancing speed, efficiency, and robustness:

❑ Shared Preferences When storing the UI state, user preferences, or application settings, you want a lightweight mechanism to store a known set of values. Shared Preferences let you save groups of key/value pairs of primitive data as named preferences.

❑ Files It’s not pretty, but sometimes writing to, and reading from, files directly is the only way to go. Android lets you create and load fi les on the device’s internal or external media.

❑ SQLite Databases When managed, structured data is the best approach, Android offers the SQLite relational database library. Every application can create its own databases over which it has total control.

❑ Content Providers Rather than a storage mechanism in their own right, Content Providers let you expose a well-defined interface for using and sharing private data. You can control access to Content Providers using the standard permission system.

Saving Simple Application Data

There are two lightweight techniques for saving simple application data for Android applications — Shared Preferences and a pair of event handlers used for saving Activity instance details. Both mechanisms use a name/value pair (NVP) mechanism to store simple primitive values. Using SharedPreferences, you can create named maps of key/value pairs within your application

that can be shared between application components running in the same Context. Shared Preferences support the primitive types Boolean, string, float, long, and integer, making the ideal way to quickly store default values, class instance variables, the current UI state, and user preferences. They are most commonly used to persist data across user sessions and to share settings between application components.

Alternatively, Activities offer the onSaveInstanceState handler. It’s designed specifically to persist the UI state when the Activity becomes eligible for termination by a resource-hungry run time. The handler works like the Shared Preference mechanism. It offers a Bundle parameter that represents a key/value map of primitive types that can be used to save the Activity’s instance values. This Bundle is then made available as a parameter passed in to the onCreate and onRestoreInstanceState method handlers.

Creating and Saving Preferences

To create or modify a Shared Preference, call getSharedPreferences on the application Context, passing in the name of the Shared Preferences to change. Shared Preferences are shared across an application’s components but aren’t available to other applications. To modify a Shared Preference, use the SharedPreferences.Editor class. Get the Editor object by calling edit on the SharedPreferences object you want to change. To save edits, call commit on the Editor, as shown in the code snippet below.

public static final String MYPREFS = “mySharedPreferences”;

protected void savePreferences(){

// Create or retrieve the shared preference object.

int mode = Activity.MODE_PRIVATE;

SharedPreferences mySharedPreferences = getSharedPreferences(MYPREFS,

mode);

// Retrieve an editor to modify the shared preferences.

SharedPreferences.Editor editor = mySharedPreferences.edit();

// Store new primitive types in the shared preferences object.

editor.putBoolean(“isTrue”, true);

editor.putFloat(“lastFloat”, 1f);

editor.putInt(“wholeNumber”, 2);

editor.putLong(“aNumber”, 3l);

editor.putString(“textEntryValue”, “Not Empty”);

// Commit the changes.

editor.commit();

}

Retrieving Shared Preferences

Accessing saved Shared Preferences is also done with the getSharedPreferences method. Pass in the name of the Shared Preference you want to access, and use the type-safe get<type> methods to extract saved values. Each getter takes a key and a default value (used when no value is available for that key), as shown in the skeleton code below:

public void loadPreferences() {

// Get the stored preferences

int mode = Activity.MODE_PRIVATE;

SharedPreferences mySharedPreferences = getSharedPreferences(MYPREFS,

mode);

// Retrieve the saved values.

boolean isTrue = mySharedPreferences.getBoolean(“isTrue”, false);

float lastFloat = mySharedPreferences.getFloat(“lastFloat”, 0f);

int wholeNumber = mySharedPreferences.getInt(“wholeNumber”, 1);

long aNumber = mySharedPreferences.getLong(“aNumber”, 0);

String stringPreference;

stringPreference = mySharedPreferences.getString(“textEntryValue”,

“”);

Saving the Activity State

If you want to save Activity information that doesn’t need to be shared with other components, you can call Activity.getPreferences() without specifying a preferences name. Access to the Shared Preferences map returned is restricted to the calling Activity; each Activity supports a single unnamed SharedPreferences object. The following skeleton code shows how to use the Activity’s private Shared Preferences:

protected void saveActivityPreferences(){

// Create or retrieve the activity preferences object.

SharedPreferences activityPreferences =

getPreferences(Activity.MODE_PRIVATE);

// Retrieve an editor to modify the shared preferences.

SharedPreferences.Editor editor = activityPreferences.edit();

// Retrieve the View

TextView myTextView = (TextView)findViewById(R.id.myTextView);

// Store new primitive types in the shared preferences object.

editor.putString(“currentTextValue”,

myTextView.getText().toString());

// Commit changes.

editor.commit();

}

Saving and Restoring Instance State

To save Activity instance variables, Android offers a specialized alternative to Shared Preferences. By overriding an Activity’s onSaveInstanceState event handler, you can use its Bundle parameter to save instance values. Store values using the same get and put methods as shown for Shared Preferences, before passing the modifi ed Bundle into the superclass’s handler, as shown in the following code snippet:

private static final String TEXTVIEW_STATE_KEY = “TEXTVIEW_STATE_KEY”;

@Override

public void onSaveInstanceState(Bundle outState) {

// Retrieve the View

TextView myTextView = (TextView)findViewById(R.id.myTextView);

// Save its state

outState.putString(TEXTVIEW_STATE_KEY,

myTextView.getText().toString());

super.onSaveInstanceState(outState);

}

This handler will be triggered whenever an Activity completes its Active life cycle, but only when it’s not being explicitly finished. As a result, it’s used to ensure a consistent Activity state between active life cycles of a single user session. The saved Bundle is passed in to the onRestoreInstanceState and onCreate methods if the application is forced to restart during a session. The following snippet shows how to extract values from the Bundle and use them to update the Activity instance state:

@Override

public void onCreate(Bundle icicle) {

super.onCreate(icicle);

setContentView(R.layout.main);

TextView myTextView = (TextView)findViewById(R.id.myTextView);

String text = “”;

if (icicle != null && icicle.containsKey(TEXTVIEW_STATE_KEY))

text = icicle.getString(TEXTVIEW_STATE_KEY);

myTextView.setText(text);

}

Saving and Loading Files

It’s good practice to use Shared Preferences or a database to store your application data, but there are still times when you’ll want to use fi les directly rather than rely on Android’s managed mechanisms. As well as the standard Java I/O classes and methods, Android offers openFileInput and openFileOuput to simplify reading and writing streams from and to local fi les, as shown in the code snippet below:

String FILE_NAME = “tempfile.tmp”;

// Create a new output file stream that’s private to this application.

FileOutputStream fos = openFileOutput(FILE_NAME, Context.MODE_PRIVATE);

// Create a new file input stream.

FileInputStream fis = openFileInput(FILE_NAME);

These methods only support fi les in the current application folder; specifying path separators will cause an exception to be thrown. If the filename you specify when creating a FileOutputStream does not exist, Android will create it for you. The default behavior for existing fi les is to overwrite them; to append an existing fi le, specify the mode as Context.MODE_APPEND. By default, fi les created using the openFileOutput method are private to the calling application — a different application that tries to access these fi les will be denied access. The standard way to share a fi le between applications is to use a Content Provider. Alternatively, you can specify either Context.MODE_WORLD_READABLE or Context.MODE_WORLD_WRITEABLE when creating the output file to make them available in other applications, as shown in the following snippet:

String OUTPUT_FILE = “publicCopy.txt”;

FileOutputStream fos = openFileOutput(OUTPUT_FILE, Context.MODE_WORLD_WRITEABLE);

Including Static Files as Resources

If your application requires external fi le resources, you can include them in your distribution package by placing them in the res/raw folder of your project hierarchy. To access these Read Only fi le resources, call the openRawResource method from your application’s Resource object to receive an InputStream based on the specifi ed resource. Pass in the file name (without extension) as the variable name from the R.raw class, as shown in the skeleton code below:

Resources myResources = getResources();

InputStream myFile = myResources.openRawResource(R.raw.myfilename);

Adding raw files to your resources hierarchy is an excellent alternative for large, preexisting data sources (such as dictionaries) where it’s not desirable (or even possible) to convert them into an Android database.

Databases in Android

Android provides full relational database capabilities through the SQLite library, without imposing any additional limitations.

Using SQLite, you can create independent, relational databases for each application. Use them to store and manage complex, structured application data.

All Android databases are stored in the /data/data/<package_name>/databases folder on your device (or emulator). By default, all databases are private, accessible only by the application that create them. To share a database across applications, use Content Providers.

SQLite

SQLite is a relational database management system (RDBMS). It is well regarded, being:

❑ Open source

❑ Standards-compliant

❑ Lightweight

❑ Single-tier

It has been implemented as a compact C library that’s included as part of the Android software stack. By providing functionality through a library, rather than as a separate process, each database becomes an integrated part of the application that created it. This reduces external dependencies, minimizes latency, and simplifies transaction locking and synchronization.

For more comprehensive coverage of SQLite, including its particular strengths and limitations, check out the official site at www.sqlite.org/.

Cursors and Content Values

ContentValues objects are used to insert new rows into database tables (and Content Providers). Each Content Values object represents a single row, as a map of column names to values. Queries in Android are returned as Cursor objects. Rather than extracting and returning a copy of the result values, Cursors act as pointers to a subset of the underlying data. Cursors are a managed way of controlling your position (row) in the result set of a database query. The Cursor class includes several functions to navigate query results including, but not limited to, the following:

❑ moveToFirst Moves the cursor to the first row in the query result.

❑ moveToNext Moves the cursor to the next row.

❑ moveToPrevious Moves the cursor to the previous row.

❑ getCount Returns the number of rows in the result set.

❑ getColumnIndexOrThrow Returns an index for the column with the specified name (throwing an exception if no column exists with that name).

❑ getColumnName Returns the name of the specified column index.

❑ getColumnNames Returns a String array of all the column names in the current cursor.

❑ moveToPosition Moves the cursor to the specified row.

❑ getPosition Returns the current cursor position.

Android provides a mechanism to manage Cursor resources within your Activities. The startManagingCursor method integrates the Cursor’s lifetime into the parent Activity’s lifetime management. When you’ve finished with the Cursor, call stopManagingCursor to do just that.

Working with Android Databases

It’s good practice to create a helper class to simplify your database interactions. Consider creating a database adapter, which adds an abstraction layer that encapsulates database interactions. It should provide intuitive, strongly typed methods for adding, removing, and updating items. A database adapter should also handle queries and wrap creating, opening, and closing the database. It’s often also used as a convenient location from which to publish static database constants, including table names, column names, and column indexes. The following snippet shows the skeleton code for a standard database adapter class. It includes an extension of the SQLiteOpenHelper class, used to simplify opening, creating, and upgrading the database.

import android.content.Context;

import android.database.*;

import android.database.sqlite.*;

import android.database.sqlite.SQLiteDatabase.CursorFactory;

import android.util.Log;

public class MyDBAdapter {

private static final String DATABASE_NAME = “myDatabase.db”;

private static final String DATABASE_TABLE = “mainTable”;

private static final int DATABASE_VERSION = 1;

// The index (key) column name for use in where clauses.

public static final String KEY_ID=”_id”;

// The name and column index of each column in your database.

public static final String KEY_NAME=”name”;

public static final int NAME_COLUMN = 1;

// TODO: Create public field for each column in your table.

// SQL Statement to create a new database.

private static final String DATABASE_CREATE = “create table “ +

DATABASE_TABLE + “ (“ + KEY_ID +

“ integer primary key autoincrement, “ +

KEY_NAME + “ text not null);”;

// Variable to hold the database instance

private SQLiteDatabase db;

// Context of the application using the database

private final Context context;

// Database open/upgrade helper

private myDbHelper dbHelper;

public MyDBAdapter(Context _context) {

context = _context;

dbHelper = new myDbHelper(context, DATABASE_NAME, null,

DATABASE_VERSION);

}

public MyDBAdapter open() throws SQLException {

db = dbHelper.getWritableDatabase();

return this;

}

public void close() {

db.close();

}

public long insertEntry(MyObject _myObject) {

ContentValues contentValues = new ContentValues();

// TODO fill in ContentValues to represent the new row

return db.insert(DATABASE_TABLE, null, contentValues);

}

public boolean removeEntry(long _rowIndex) {

return db.delete(DATABASE_TABLE, KEY_ID +

“=” + _rowIndex, null) > 0;

}

public Cursor getAllEntries () {

return db.query(DATABASE_TABLE, new String[] {KEY_ID, KEY_NAME},

null, null, null, null, null);

}

public MyObject getEntry(long _rowIndex) {

MyObject objectInstance = new MyObject();

// TODO Return a cursor to a row from the database and

// use the values to populate an instance of MyObject

return objectInstance;

}

public int updateEntry(long _rowIndex, MyObject _myObject) {

String where = KEY_ID + “=” + _rowIndex;

ContentValues contentValues = new ContentValues();

// TODO fill in the ContentValue based on the new object

return db.update(DATABASE_TABLE, contentValues, where, null);

}

private static class myDbHelper extends SQLiteOpenHelper {

public myDbHelper(Context context, String name,

CursorFactory factory, int version) {

super(context, name, factory, version);

}

// Called when no database exists in

// disk and the helper class needs

// to create a new one.

@Override

public void onCreate(SQLiteDatabase _db) {

_db.execSQL(DATABASE_CREATE);

}

// Called when there is a database version mismatch meaning that

// the version of the database on disk needs to be upgraded to

// the current version.

@Override

public void onUpgrade(SQLiteDatabase _db, int _oldVersion,

int _newVersion) {

// Log the version upgrade.

Log.w(“TaskDBAdapter”, “Upgrading from version “ +

_oldVersion + “ to “ +

_newVersion +

“, which will destroy all old data”);

// Upgrade the existing database to conform to the new version.

// Multiple previous versions can be handled by comparing

// _oldVersion and _newVersion values.

// The simplest case is to drop the old table and create a

// new one.

_db.execSQL(“DROP TABLE IF EXISTS “ + DATABASE_TABLE);

// Create a new one.

onCreate(_db);

}

}

}

Using the SQLiteOpenHelper

SQLiteOpenHelper is an abstract class that wraps up the best practice pattern for creating, opening, and upgrading databases. By implementing and using an SQLiteOpenHelper, you hide the logic used to decide if a database needs to be created or upgraded before it’s opened.

Call getReadableDatabase or getWriteableDatabase to open and return a readable/writable instance of the database. A call to getWriteableDatabase can fail because of disk space or permission issues, so it’s good practice to provide fallback to the getReadableDatabase method as shown below:

dbHelper = new myDbHelper(context, DATABASE_NAME, null, DATABASE_VERSION);

SQLiteDatabase db;

try {

db = dbHelper.getWritableDatabase();

}

catch (SQLiteException ex){

db = dbHelper.getReadableDatabase();

}

Opening and Creating Databases without the SQLiteHelper

You can create and open databases without using the SQLiteHelper class with the openOrCreateDatabase method on the application Context. Setting up a database is a two-step process. First, call openOrCreateDatabase to create the new database.

Then, call execSQL on the resulting database instance to run the SQL commands that will create your tables and their relationships. The general process is shown in the snippet below:

private static final String DATABASE_NAME = “myDatabase.db”;

private static final String DATABASE_TABLE = “mainTable”;

private static final String DATABASE_CREATE =

“create table “ + DATABASE_TABLE +

“ ( _id integer primary key autoincrement,” +

“column_one text not null);”;

SQLiteDatabase myDatabase;

private void createDatabase() {

myDatabase = openOrCreateDatabase(DATABASE_NAME,

Context.MODE_PRIVATE, null);

myDatabase.execSQL(DATABASE_CREATE);

}

Querying Your Database

All database queries are returned as a Cursor to a result set. This lets Android manage resources more efficiently by retrieving and releasing row and column values on demand. To execute a query on a database, use the query method on the database object, passing in:

❑ An optional Boolean that specifies if the result set should contain only unique values

❑ The name of the table to query

❑ A projection, as an array of Strings, that lists the columns to include in the result set

❑ A “where” clause that defines the rows to be returned. You can include wildcards that will be replaced by the values stored in the selection argument parameter.

❑ An array of selection argument strings that will replace the ?’s in the “where” clause

❑ A “group by” clause that defines how the resulting rows will be grouped

❑ A “having” filter that defines which row groups to include if you specified a “group by” clause

❑ A String that describes the order of the returned rows

❑ An optional String that defines a limit to the returned rows

The following skeleton code shows snippets for returning some, and all, of the rows in a particular table:

// Return all rows for columns one and three, no duplicates

String[] result_columns = new String[] {KEY_ID, KEY_COL1, KEY_COL3};

Cursor allRows = myDatabase.query(true, DATABASE_TABLE, result_columns,

null, null, null, null, null, null);

// Return all columns for rows where column 3 equals a set value

// and the rows are ordered by column 5.

String where = KEY_COL3 + “=” + requiredValue;

String order = KEY_COL5;

Cursor myResult = myDatabase.query(DATABASE_TABLE, null, where,

null, null, null, order);