CHAPTER 7
The majority of Android Programming Succinctly has focused on developing the user interface for an Android app. Understanding how to manage the activity lifecycle, display information, collect input, and lay out screens will go a long way towards creating your first Android app. However, most apps also need a way to store the data they collect. In this chapter, we’ll take a brief look at several of Android’s data storage options.
First, we’ll look at shared preferences, which are simple key-value pairs that persist outside of your application. Then, we’ll learn how to access Android’s internal storage. Finally, we’ll introduce Android’s SQLite API. The ApplicationData project provides a working example of all three of these storage mechanisms.
Android’s shared preferences framework is the easiest way to store information between user sessions. It allows you to store primitive data (Booleans, floats, ints, longs, and strings) using key-value pairs, much like a persistent hashtable. An activity can have one or more SharedPreferences objects associated with it.
The SharedPreferences class provides access to stored values, and the SharedPreferences.Editor class lets you modify those values. To store values with SharedPreferences, you first need to get access to the shared preferences for the activity. If you only need one preferences file for an activity, you should use getPreferences(), but if you need multiple files, you can specify the preference file names using the getSharedPreferences() method.
Both of these methods also let you specify who is allowed to access and modify the preference file. While it’s possible to create preference files that are accessible from other apps using the MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE, this can open up security vulnerabilities in your application. So, you should always use MODE_PRIVATE as the scope for preference files. If you need to share stored values with other apps, you should use something like ContentProvider.
Once you have an instance of SharedPreferences, you can read saved values by passing the desired key to methods like getBoolean(), getInt(), getFloat(), and getString(). To record values, you first need to get a SharedPreferences.Editor object by calling edit() on the SharedPreferences instance. Then, you can set key-value pairs with methods like putBoolean(), putFloat(), etc. Finally, you must call the editor’s commit() method to save any updated values.
The following version of MainActivity.java shows you how to record input from an <EditText> element using SharedPreferences and display that value when the activity loads:
package com.example.applicationdata;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.view.KeyEvent;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.EditText;
import android.content.SharedPreferences;
public class MainActivity extends Activity {
private static String SHARED_PREFS_KEY = "existingInput";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Set up the EditText
EditText prefsText = (EditText) findViewById(R.id.sharedPrefsText);
prefsText.setOnEditorActionListener(new OnEditorActionListener() {
public boolean onEditorAction(TextView textView,
int actionId,
KeyEvent event) {
String input = textView.getText().toString();
saveStringWithSharedPreferences(SHARED_PREFS_KEY, input);
return false;
}
});
// Load the string from SharedPreferences
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
String existingInput = prefs.getString(SHARED_PREFS_KEY, "");
prefsText.setText(existingInput);
}
public void saveStringWithSharedPreferences(String key, String value) {
// Get the SharedPreferences editor.
SharedPreferences prefs = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
// Save the string.
editor.putString(key, value);
// Commit the changes.
editor.commit();
}
}
Note that SharedPreferences’s getter methods let you specify a default value as their second parameter, as you can see in the pref.getString(SHARED_PREFS_KEY, "") call in the above code.
While SharedPreferences offers a convenient abstraction for storing simple data, it’s not always appropriate for more complex data structures or for recording a user’s documents. As an alternative, Android apps can store information directly on the device’s hard drive. However, like SharedPreferences, these files are private—other apps shouldn’t be allowed to access them due to security vulnerabilities. Files saved to internal storage are deleted when your app in uninstalled.
To save data to a file, you first need to open the file with Context.openFileOutput(). This returns a FileOutputStream, whose write() method enables you to add bytes to the file. When you’re done writing data to the file, you have to close it with its close() method. The following method shows you how to store a string in a file:
public void saveStringWithInternalStorage(String filename,
String value) throws IOException {
FileOutputStream output = openFileOutput(filename, MODE_PRIVATE);
byte[] data = value.getBytes();
output.write(data);
output.close();
}
Note that FileOutputStream works with bytes, so the string has to be converted before passing it to write().
To read back this string data, you need to open the file with openFileInput(), which returns a FileInputStream object. Then you can read the file contents into a byte array. When you’re done, don’t forget to close the input stream. The following example loads the string saved by the previous snippet and displays it in an EditText widget:
FileInputStream input = null;
try {
// Open the file.
input = openFileInput(FILENAME);
// Read the byte data.
int maxBytes = input.available();
byte[] data = new byte[maxBytes];
input.read(data, 0, maxBytes);
while (input.read(data) != -1) {};
// Turn it into a String and display it.
String existingInput = new String(data);
prefsText.setText(existingInput);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
The FileInputStream.available() method returns the estimated number of bytes that can be read without blocking for more input. We use this to figure out the length of a new byte array, which is populated by FileInputStream.read(). These bytes are then converted to a string and displayed to the user.
Android also provides built-in support for local SQLite databases. This is useful for data-intensive applications that need to be able to collect large amounts of information and query it efficiently. This section shows you how to create SQLite database on an Android device, create a table, insert data into it, and query it using SQL.
Mixing raw SQL with the rest of your application code can get messy very quickly, so Android uses certain conventions to abstract the database interaction as much as possible from your application code. For example, each table should be represented by a dedicated class. This provides a single place to define your table and column names, as well as a standardized way to create, upgrade, and access the table.
To create the class representation for a particular table, you need to extend the SQLiteOpenHelper class and override its onCreate() and onUpgrade() methods to create the table and upgrade it to a new version, respectively. It’s also customary to define the table name and all of the columns as static final variables in this class. For example, a table called messages stored in the SQLite database file messages.db might be represented by a class called MessageOpenHelper that looks like this:
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteDatabase.CursorFactory;
import android.database.sqlite.SQLiteOpenHelper;
public class MessageOpenHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "messages.db";
public static final String TABLE_MESSAGES = "messages";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_AUTHOR = "author";
public static final String COLUMN_MESSAGE = "message";
private static final String DATABASE_CREATE = "create table "
+ TABLE_MESSAGES + "("
+ COLUMN_ID + " integer primary key autoincrement, "
+ COLUMN_AUTHOR + " text not null"
+ COLUMN_MESSAGE + " text not null);";
public MessageOpenHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DATABASE_CREATE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This implementation will destroy all the old data, which
// probably isn't what you want to do in a real application.
db.execSQL("drop table if exists " + TABLE_MESSAGES);
onCreate(db);
}
}
The entire table is defined by the final static variables at the beginning of the class. DATABASE_VERSION and DATABASE_NAME define the SQLite database version and filename, while the following four lines define a table called messages with three columns called _id, author, and message. Finally, the DATABASE_CREATE variable contains the raw SQL to create the messages table. This is one of the only places you’ll be writing raw SQL, and it’s neatly contained in a static final variable. Again, this structure is more of a suggested convention than a hard-and-fast rule.
The constructor for the class needs to pass the database name and version to the superclass. The job of the onCreate() method is to initialize the SQL table associated with the class. It is passed an SQLiteDatabase instance representing the database, and all we have to do is execute the DATABASE_CREATE string that we defined earlier using its execSQL() method. Similarly, the job of the onUpgrade() method is to upgrade the table to a new version. The above implementation simply drops the table and recreates it, which may or may not be desirable for your real-world application.
SQLiteOpenHelper methods make it very easy to create and access the underlying database. All you have to do is instantiate your custom SQLiteOpenHelper and request a database with either getReadableDatabase() or getWritableDatabase(). For example, to create a database called messages.db and connect to it, all you need is the following:
MessageOpenHelper dbHelper = new MessageOpenHelper(this);
SQLiteDatabase db = dbHelper.getWritableDatabase();
The getReadableDatabase() and getWritableDatabase() methods are responsible for creating the underlying SQLite database if it doesn’t already exist and opening it for reading and/or writing. The onCreate() method defined by MessageOpenHelper is used to create the database. The returned SQLiteDatabase object provides methods for altering and querying tables in the database.
You should always close the database by calling close() on your SQLiteOpenHelper instance when you are done interacting with the database.
Inserting rows into an SQLiteDatabase object is a two-step process. First, you need to create a ContentValues object to represent the values that you want to insert. A single ContentValues instance represents a single record, and you define it by passing each column and value to its put() method. Typically, you’ll want to take the column names from the static variables defined in your custom SQLiteOpenHelper.
Second, you need to pass that ContentValues object to the SQLiteDatabase’s insert() method, along with the name of the table that you want to insert into. For example, the following method (which is defined in the example project’s MainActivity.java) adds a new record to the messages table each time it is called.
public void saveStringWithDatabase(String value) {
// Store the author and message in a ContentValues object.
ContentValues values = new ContentValues();
values.put(MessageOpenHelper.COLUMN_AUTHOR, AUTHOR_NAME);
values.put(MessageOpenHelper.COLUMN_MESSAGE, value);
// Record that ContentValues in a SQLite database
MessageOpenHelper dbHelper = new MessageOpenHelper(this);
SQLiteDatabase db = dbHelper.getWritableDatabase();
long id = db.insert(MessageOpenHelper.TABLE_MESSAGES, null, values);
Log.d(TAG,
String.format("Saved new record to database with ID: %d", id));
dbHelper.close();
}
Notice how we called dbHelper.close() call when we were done using the database. Also notice how we access the table name via MessageOpenHelper’s static variable so that it isn’t accidentally mistyped.
To retrieve records from an SQLiteDatabase instance, you pass your query information to one of its query() methods. The various query() overloads provide parameters to all of the standard SQL query parameters. You can define the columns to select, the selection constraints, the grouping and ordering behavior, and selection limits.
Records are returned as Cursor objects, which you can use to iterate through the selected rows and cast the contained values to Java types. For example, the following snippet opens messages.db and selects the _id and message columns from rows that have AUTHOR_NAME in the author column:
// Load the most recent record from the SQLite database.
MessageOpenHelper dbHelper = new MessageOpenHelper(this);
SQLiteDatabase db = dbHelper.getReadableDatabase();
// Fetch the records with the appropriate author name.
String[] columns = {MessageOpenHelper.COLUMN_ID,
MessageOpenHelper.COLUMN_MESSAGE};
String selection = MessageOpenHelper.COLUMN_AUTHOR + " = '" + AUTHOR_NAME + "'";
Cursor cursor = db.query(MessageOpenHelper.TABLE_MESSAGES,
columns, selection, null, null, null, null);
// Display the most recent record in the text field.
cursor.moveToLast();
long id = cursor.getLong(0);
String message = cursor.getString(1);
Log.d(TAG, String.format("Retrieved info from database. ID: %d Message: %s", id, message));
prefsText.setText(message);
// Clean up.
cursor.close();
dbHelper.close();
The moveToLast() method moves the cursor to the last selected row, which in this case should be the most recent record. To extract the values, you use methods like getLong() and getString(), passing in the column position. Note that this position is defined by the columns array that we passed to query(), not the order of the columns in the database. When we were done with the results, we cleaned up by calling close() on both cursor and the SQLiteOpenHelper.
While this section only covered the basics of Android’s SQLite API, keep in mind that Android also provides more advanced SQL functionality, including database locking and transactions.
This chapter discussed three of the most common ways to store data on an Android device. We began with shared preferences, which provide a convenient way to store key-value pairs. Then, we learned how to store data in files on the device’s internal storage, which is more flexible than shared preferences. Finally, we took a brief look at Android’s built-in SQLite capabilities by creating an SQLite database, inserting some rows, and reading them back out.
The majority of this book discussed how to display and collect information from the user. Combined with this chapter, you should now be able to collect and store almost any kind of user data you could possibly need. I hope that, armed with these skills, you’re feeling ready to venture out into the Android ecosystem and start building your own Android applications. Good luck!