Chapter - 7
Data Persistence
Persisting data is very important in application development, as users typically expect to
reuse data in the future. For Android, there are primarily three basic ways of persisting
data
1. A lightweight mechanism known as shared preferences to save small chunks of data
2. Traditional file systems
3. A relational database management system through the support of SQLite databases
PERSISTING DATA TO FILES
In the previous chapter you had seen how to use shared preference to persist user
preference as key value pairs.
In this example you will explore how to write data to files using java.io package
Hands on:
1. Using Eclipse, create an Android project and name FileIO
2. Add the following to activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Please enter some text" />
<EditText
android:id="@+id/txtText1"
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btnSave"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="saveData"
android:text="Save" />
<Button
android:id="@+id/btnLoad"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:onClick="loadData"
android:text="Load" />
3. Modify the MainActivity.java as follows
public class MainActivity extends Activity {
EditText textBox;
static final int READ_BLOCK_SIZE = 100;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textBox = (EditText) findViewById(R.id.txtText1);
}
public void saveData(View v)
{
String input = textBox.getText().toString();
try
{
FileOutputStream fOut = openFileOutput("data.txt", MODE_PRIVATE);
OutputStreamWriter writer = new OutputStreamWriter(fOut);
writer.write(input);
writer.flush();
writer.close();
}
catch (FileNotFoundException fnfe)
{
Log.e("FileIO", fnfe.getMessage());
}
catch(IOException ioe)
{
Log.e("FileIO", ioe.getMessage());
}
Toast.makeText(getBaseContext(), "File Saved successfully", Toast.LENGTH_SHORT).show();
textBox.setText("");
}
public void loadData(View v)
{
try
{
FileInputStream fIn = openFileInput("data.txt");
InputStreamReader reader = new InputStreamReader(fIn);
char[] inputBuffer = new char[READ_BLOCK_SIZE];
String str = "";
int charRead;
while ((charRead = reader.read(inputBuffer))>0)
{
//---convert the chars to a String---
String readString = String.copyValueOf(inputBuffer, 0, charRead);
str += readString;
inputBuffer = new char[READ_BLOCK_SIZE];
}
textBox.setText(str);
}
catch (FileNotFoundException fnfe) {
Log.e("FileIO", fnfe.getMessage());
}
catch(IOException ioe)
{
Log.e("FileIO", ioe.getMessage());
}
}
}
4. Save the project and run it in emulator. Enter text and click on save
button.
5. The text entered will get saved.
6. Now click on Load button and whatever text was saved into the file, will be
retrieved and displayed.
7. If you look in the File Explorer ( available in DDMS view ), you can see the file
data.txt created under /data/data/com.sysdomain.fileio/files
Code Explanation:
To save text into a file, you use the FileOutputStream class. The openFileOutput()
method opens a named file for writing, with the mode specified. MODE_PRIVATE will
allow only the application which created it to access it.
FileOutputStream fOut = openFileOutput("data.txt", MODE_PRIVATE);
To convert a character stream into a byte stream, you use an instance of the
OutputStreamWriter class, by passing it an instance of the FileOutputStream object.
You then use its write() method to write the string to the fi le. To ensure that all the
bytes are written to the file, use the flush() method. Finally, use the close() method to
close the file
OutputStreamWriter writer = new
OutputStreamWriter(fOut);
writer.write(input);
writer.flush();
writer.close();
To read the content of a file, you use the FileInputStream class, together with the
InputStreamReader class
FileInputStream fIn = openFileInput("data.txt");
InputStreamReader reader = new InputStreamReader(fIn);
Since you do not know the size of the file to read, the content is read in blocks of 100
characters into a buffer (character array). The characters read are then copied into a
String object
char[] inputBuffer = new char[READ_BLOCK_SIZE];
String str = "";
int charRead;
while ((charRead = reader.read(inputBuffer))>0)
{
//---convert the chars to a String---
String readString = String.copyValueOf(inputBuffer, 0,
charRead);
str += readString;
inputBuffer = new char[READ_BLOCK_SIZE];
}
The read() method of the InputStreamReader object checks the number of characters
read and returns -1 if the end of the file is reached
Saving to External Storage (SD Card)
The previous section showed how you can save your files to the internal storage of your Android device.
Sometimes, it would be useful to save them to external storage (such as an SD card) because of its larger
capacity, as well as the capability to share the files easily with other users (by removing the SD card and
passing it to somebody else).
Hands On:
Note: For this example you need a AVD with SD card.
Let’s use the previously created project and make modifications to the saveData() method.
public void saveData(View v)
{
String input = textBox.getText().toString();
// get the SD card storage
File sdCard = Environment.getExternalStorageDirectory();
File mydirectory = new File(sdCard.getAbsolutePath() + "/MyFiles");
if(!mydirectory.exists())
{
mydirectory.mkdir();
}
File file = new File(mydirectory, "data.txt");
try
{
FileOutputStream fOut = new FileOutputStream(file);
//FileOutputStream fOut = openFileOutput("data.txt", MODE_PRIVATE);
OutputStreamWriter writer = new OutputStreamWriter(fOut);
writer.write(input);
writer.flush();
writer.close();
}
catch (FileNotFoundException fnfe)
{
Log.e("FileIO", fnfe.getMessage());
}
catch(IOException ioe)
{
Log.e("FileIO", ioe.getMessage());
}
Toast.makeText(getBaseContext(), "File Saved successfully",
Toast.LENGTH_SHORT).show();
textBox.setText("");
}
You use the getExternalStorageDirectory() method to return the full path to the external
storage.
It should return the “/sdcard” path for a real device, and “/mnt/sdcard” for an Android
emulator.
Caution: You should never try to hardcode the path to the SD card, as manufacturers
may choose to assign a different path name to the SD card. Always use
getExternalStorageDirectory().
You then create a directory called MyFiles in the SD card and then you save the file into
this directory.
Note: In order to write to the external storage, you need to add the
WRITE_EXTERNAL_STORAGE permission in your AndroidManifest.xml file
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sysdomain.fileio"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="17"
android:targetSdkVersion="17" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.sysdomain.fileio.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Now let’s modify loadData() method to read data from external SD card.
public void loadData(View v)
{
File sdCard = Environment.getExternalStorageDirectory();
File directory = new File(sdCard.getAbsolutePath() + "/MyFiles");
File datafile = new File(directory, "data.txt");
try
{
FileInputStream fIn = new FileInputStream(datafile);
InputStreamReader reader = new
InputStreamReader(fIn);
//FileInputStream fIn = openFileInput("data.txt");
//InputStreamReader reader = new
InputStreamReader(fIn);
.
.
.
//other code remains as it is
You can check the file which gets created under /mnt/sdcard/MyFiles
Choosing the Best Storage Option
The previous sections described three main ways to save data in your Android
applications: the SharedPreferences object, internal storage, and external storage.
Which one should you use in your applications? Here are some guidelines
If you have data that can be represented using name/value pairs, then use the
SharedPreferences object. For example, if you want to store user preference data such
as user name, background color, text size, or last login date, then the SharedPreferences
object is the ideal way to store this data.
If you need to store ad-hoc data, then using the internal storage is a good option. For
example, your application (such as an RSS reader) may need to download images from
the web for display. In this scenario, saving the images to internal storage is a good
solution. You may also need to persist data created by the user, such as when you have
a note-taking application that enables users to take notes and save them for later use.
In both of these scenarios, using the internal storage is a good choice.
In case you need to share data with other applications or systems, for example if you
have an Android application which will record location co-ordinates and at the end of
the day all the data need to be uploaded to a server component, then its better to use
external storage.
CREATING AND USING DATABASES
The previous examples showed you ways to persist simple data sets. But in case of
more structured data, it’s always better to go with Relational database. Relational
databases have proven to be the most preferred choice for storing complex data sets
and also have mechanisms to enforce data integrity.
For example, if you are going to build an Android app which is going to be used by sales
reps to record order information, then it’s better to go with relational database to
structure and store data. This way you can run queries and prepare consolidated
reporting as well. E.g: Total sales for given day etc.
Android uses the SQLite database system. The database that you create for an
application is only accessible to itself; other applications will not be able to access it.
DBAdapter Helper Class
A good practice for dealing with databases is to create a helper class to encapsulate all
the complexities of accessing the data so that it is transparent to the calling code. Hence
you will create a helper class called DBAdapter that creates, opens, closes, and uses a
SQLite database.
Let’s create a database named StudentsDB containing one table named Student with
three columns _id, name and email.
Hands On:
1. Using Eclipse, create an Android project and name it DBExample
2. Right click on java package and add java class file and name it MySQLiteHelper.java
3. Make the following changes to MySQLiteHelper.java
public class MySQLiteHelper extends SQLiteOpenHelper {
private static final String TAG = "MySQLiteHelper";
private static final String DB_NAME = "StudentDB.db";
private static final int DB_VERSION = 1;
public static final String TABLE_STUDENT = "student";
public static final String COL_ROWID = "_id";
public static final String COL_NAME = "name";
public static final String COL_EMAIL = "email";
private static final String DB_CREATE = "create table student
(_id integer primary key autoincrement, " +
" name text not null, email text not null);";
//constructor
public MySQLiteHelper(Context context)
{
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
try
{
db.execSQL(DB_CREATE);
}
catch (SQLException sql)
{
Log.e(TAG, "Error during database create");
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w("MySQLiteHelper", "Upgrading database from version " + oldVersion
+ " to " + newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS student");
onCreate(db);
}
public SQLiteDatabase open() throws SQLException
{
return super.getWritableDatabase();
}
}
Code Explanation:
The MySQLiteHelper class extends android.database.sqlite.SQLiteOpenHelper – which
is a helper class to manage database creation and version management .
Then you overide onCreate() and onUpgrade() methods as follows.
OnCreate() method is called when the database is created for the first time. This is
where the creation of tables and the initial population of the tables should happen.
@Override
public void onCreate(SQLiteDatabase db) {
try
{
db.execSQL(DB_CREATE);
}
catch (SQLException sql)
{
Log.e(TAG, "Error during database create");
}
}
The DB_CREATE string contains the Data Definition Language (SQL) for creating table.
String DB_CREATE = "create table student (_id integer primary key autoincrement, " +
" name text not null, email text not null);";
OnUpgrade() method is called when the database needs to be upgraded. The
implementation should use this method to drop tables, add tables, or do anything else
it needs to upgrade to the new schema version.
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w("MySQLiteHelper", "Upgrading database from version " +
oldVersion + " to " + newVersion + ", which will destroy all old
data");
db.execSQL("DROP TABLE IF EXISTS student");
onCreate(db);
}
In the upgrade method, you drop existing tables and create the tables again.
In the open() method, you make a call to SQLiteOpenHelper.getWritableDatabase()
method which basically opens a database that will be used for reading and writing.
Note: Make sure to call close() when you no longer need the database.
4. Right click on the java package and create a java class. Name it as StudentDAO
5. Make the following changes into StudentDAO
public class StudentDAO {
private static final String TAG = "StudentDAO";
private MySQLiteHelper dbHelper;
private SQLiteDatabase db;
//constructor
public StudentDAO(Context context)
{
dbHelper = new MySQLiteHelper(context);
}
public long insert(String name, String email)
{
ContentValues cv = new ContentValues();
cv.put(MySQLiteHelper.COL_NAME, name);
cv.put(MySQLiteHelper.COL_EMAIL, email);
db = dbHelper.open();
long result = db.insert(MySQLiteHelper.TABLE_STUDENT, null, cv);
db.close();
Log.d(TAG, "Student data insert successful**");
return result;
}
}
Code Explanation:
StudentDAO class follows the Data Access Object pattern as per which all database operations to a table like
insert/select/delete/update should be done in a DAO helper class. One DAO class should ideally write to one
table only. So for example, if you have 3 tables, then you should ideally write 3 DAO classes.
Let’s initially start with just insert method, which is going to take student name and email id and insert into
Student table.
First you create a ContentValues object to store key/value pairs, which in this case is column name / values to be
inserted.
You open the database and then make an insert. In case of error the return value will contain -1. Otherwise it
will contain the row id of the inserted row.
6. Now make the following changes to MainActivity.java
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StudentDAO dao = new StudentDAO(this);
long result = dao.insert("Sadanand", "
[email protected]");
if( result != -1)
{
Toast.makeText(getBaseContext(), "Data insert successful: Row ID:" + result,
Toast.LENGTH_SHORT).show();
}
}
}
Code Explanation:
StudentDAO class is instantiated by passing a context object. Then insert method of the DAO is
called by passing the arguments.
If the result is not equal to -1, the display the success message along with row id of newly inserted
row.
7. Lets add another method getAllStudents to StudentDAO
public ArrayList<String> getAllStudents()
{
ArrayList<String> studentNameList = new ArrayList<String>();
//open database
db = dbHelper.open();
//fire the query
Cursor cursor = db.query(MySQLiteHelper.TABLE_STUDENT,
new String[] {MySQLiteHelper.COL_ROWID,
MySQLiteHelper.COL_NAME,
MySQLiteHelper.COL_EMAIL},
null, null, null, null, null);
//check if the cursor is not empty
if(cursor.moveToFirst())
{
do
{
// retrieve student ID and Name from cursor
String studentId = cursor.getString(0);
String studentName = cursor.getString(1);
String studentEmail = cursor.getString(2);
studentNameList.add("Id:" + studentId +
"\n Name:" +
studentName +
"\n Email:" +
studentEmail);
}while(cursor.moveToNext());
//close cursor
cursor.close();
}
//close database
db.close();
return studentNameList;
}
Code Explanation:
The getAllStudents() method is returning an ArrayList of Strings. Firstly the database is opened and then
query is fired.
You will use the below method call which will return a Cursor. Think of the Cursor as a pointer to the
result set from a database query.
public Cursor query (String table, String[] columns, String selection, String[] selectionArgs, String groupBy,
String having, String orderBy)
Parameters Details
Table: The table name to compile the query against.
Columns: A list of which columns to return. Passing null will return all columns, which
is discouraged to prevent reading data from storage that isn't going to be used.
Selection: A filter declaring which rows to return, formatted as an SQL WHERE clause
(excluding the WHERE itself). Passing null will return all rows for the given table.
cursor.moveToFirst()
The call to moveToFirst returns true if successful, this method will return false if the
cursor is empty.
Now retrieve the values from cursor
String studentId = cursor.getString(0);
Loop this until you have exhausted all the rows, When you reach end of last row, the
call to cursor.moveToNext() will return false.
Note: You can also use raw query if you are comfortable with SQL as shown below
Cursor cursor = db.rawQuery("select _id, name from student ", null);
8. Now make changes in ActivityMain.xml to display the results returned
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
StudentDAO dao = new StudentDAO(this);
/*long result = dao.insert("Sadanand", "
[email protected]");
if( result != -1)
{
Toast.makeText(getBaseContext(), "Data insert successful: Row ID:" + result,
Toast.LENGTH_SHORT).show();
}
*/
ArrayList<String> studentList = dao.getAllStudents();
for(String name : studentList)
{
Toast.makeText(getBaseContext(), name,
Toast.LENGTH_SHORT).show();
}
}
}
9. Save the project and run in emulator
10. Similarly add update method and delete method to StudentDAO class
public boolean updateStudent(long rowId, String name, String email)
{
ContentValues cv = new ContentValues();
cv.put(MySQLiteHelper.COL_NAME, name);
cv.put(MySQLiteHelper.COL_EMAIL, email);
db = dbHelper.open();
int result = db.update(MySQLiteHelper.TABLE_STUDENT, cv,
MySQLiteHelper.COL_ROWID + "=" + rowId, null);
db.close();
return result > 0;
}
public boolean deleteStudent(long rowId)
{
db = dbHelper.open();
int result = db.delete(MySQLiteHelper.TABLE_STUDENT,
MySQLiteHelper.COL_ROWID + "=" + rowId, null);
db.close();
return result > 0;
}
Code Explanation:
public int update(String table, ContentValues values, String whereClause, String[] whereArgs)
Convenience method for updating rows in the database.
Parameters
Table - The table to update in
Values - A map from column names to new column values. null is a valid value that will be translated to
NULL.
WhereClause - The optional WHERE clause to apply when updating. Passing null will update all rows.
WhereArgs - You may includes in the where clause, which will be replaced by the values from
whereArgs. The values will be bound as Strings.
public int delete (String table,String whereClause, String[] whereArgs)
Convenience method for deleting rows in the database.
Parameters
Table - The table to delete from
WhereClause - The optional WHERE clause to apply when deleting. Passing null will delete all rows.
WhereArgs - You may includes in the where clause, which will be replaced by the values from
whereArgs. The values will be bound as Strings.
Upgrading the Database
Sometimes, after creating and using the database, you may need to add additional
tables, change the schema of the database, or add columns to your tables. In this case,
you need to migrate your existing data from the old database to a newer one.
To upgrade the database, change the DATABASE_VERSION constant to a value higher
than the previous one. For example, if its previous value was 1, change it to 2
public class MySQLiteHelper extends SQLiteOpenHelper {
private static final String TAG = "MySQLiteHelper";
private static final String DB_NAME = "StudentDB.db";
private static final int DB_VERSION = 2;
You can see in the LogCat window that the onUpgrade() method was fired since the
version of DB was changed.
Pre-Creating the Database
Sometimes it would be more efficient to pre-create the database at design time rather
than runtime. For example, you want to create an application to log the coordinates of
all the places that you have been to. In this case, it is much easier to pre-create the
database during the design time and simply use it during runtime.
To pre-create a SQLite database, you can use many of the free tools available on the
Internet. One such tool is the SQLite Database Browser, which is available free for
different platforms
(http://sourceforge.net/projects/sqlitebrowser/).
Once you have installed the SQLite Database Browser, you can create a database visually
as shown in figure below:
Populating the data can be done as shown in figure below
You can also use the query executor to insert data
Bundling a Database
Hands On:
1. Let’s use the same project used earlier, drag and drop the SQLite database which you created in
previous section into assets folder of Android project. Make sure you select the 'Copy' option
when you drag and drop
2. Make the following changes in MainActivity.java
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try
{
String destPath = "/data/data/" + getPackageName() + "/databases";
File f = new File(destPath);
if (!f.exists()) {
f.mkdirs();
f.createNewFile();
// ---copy the db from the assets folder into
// the databases folder---
CopyDB(getBaseContext().getAssets().open("newstudentdb.db"),
new FileOutputStream(destPath));
}
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
StudentDAO dao = new StudentDAO(this);
ArrayList<String> studentList = dao.getAllStudents();
for (String name : studentList) {
Toast.makeText(getBaseContext(), name,
Toast.LENGTH_SHORT).show();
}
}
private void CopyDB(InputStream inputStream, OutputStream outputStream) {
//---copy 1K bytes at a time---
byte[] buffer = new byte[1024];
int length;
try
{
while ((length = inputStream.read(buffer)) > 0)
{
outputStream.write(buffer, 0, length);
}
inputStream.close();
outputStream.close();
} catch (IOException e)
{
Log.e("DB Example", e.getMessage());
}
}
}
3. Save the project and run on the emulator. You can see that now the
database is now copied into
/data/data/com.systemdomain.dbexample/databases folder
Code Explanation:
You first defined the CopyDB() method to copy the database fi le from one location to another
private void CopyDB(InputStream inputStream, OutputStream outputStream) {
//---copy 1K bytes at a time---
byte[] buffer = new byte[1024];
int length;
try
{
while ((length = inputStream.read(buffer)) > 0)
{
outputStream.write(buffer, 0, length);
}
inputStream.close();
outputStream.close();
} catch (IOException e)
{
Log.e("DB Example", e.getMessage());
}
}
Note that in this case you used the InputStream object to read from the source file, and then wrote
it to the destination file using the OutputStream object
When the activity is created, you copy the database file located in the assets folder into the /data/
data/com.sysdomain.dbexample/databases folder
String destPath = "/data/data/" + getPackageName() + "/databases";
File f = new File(destPath);
if (!f.exists()) {
f.mkdirs();
f.createNewFile();
// ---copy the db from the assets folder into
// the databases folder---
CopyDB(getBaseContext().getAssets().open("newstudentdb.db"),
new FileOutputStream(destPath));
How to browse SQLite data in Eclipse DDMS View
1. You need to download Questoid SQLite Browser plugin using the link
https://dl.dropboxusercontent.com/u/91846918/sqlite%20manager/com.questoid.sqlitemanager_1.0.0
.jar
2. Place the plugin .jar file in your Eclipse Dropins folder --> eclipse-jee-indigo-SR2-win32-
x86_64\eclipse\dropins
3. Restart Eclipse
4. Start up Android Emulator
5. Open DDMS perspective in Eclipse
6. Go to File Explorer tab
7. Locate the project folder /data/data/com.systemdomain.dbexample/databases
8. Open the database file as shown in figure below
9. You can switch between DataStructure and Browse Data tabs as shown
below
Assignment: Write an app which will take student registrations, and also display
student details.