Android - Data Persistence
Data Persistence
Persisting data is an important topic in application development
- because users typically expect to reuse data in the future
For Android, there are primarily three basic ways of persisting data:
- A lightweight mechanism known as shared preferences to save small chunks of data
- Traditional file systems
- A relational database management system through the support of SQLite databases
SharedPreferences
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 username, background color, date of birth, or last login date, then the SharedPreferences object is the ideal way to store this data
Android provides the SharedPreferences object to help you save simple application data.
- SharedPreferences allow you to share data across multiple apps
There are 2 ways to use SharedPreferences:
- Use the PreferenceActivity class to create preferences and modify data during runtime
- Programmatically retrieving and modifying the preferences values using the SharedPreferences class
Creating SharedPreferences
Programmatically retrieving and modifying the preferences values using the SharedPreferences class
Define SharedPreferences
1 | public static int MODE = Context.MODE_PRIVATE; |
Save SharedPreferences
1 | private void savePreferences(){ |
Data is stored in xml format, located at
/data/data/<package name>/shared_prefs/SaveSetting.xml
.Or just type “Device File Explorer” in “Help”.
Load SharedPreferences
1 | private void showPreferences(){ |
Share data across multiple Apps
To allow the access of SharedPreferences of another app, 3 conditions must be satisfied:
- The MODE of the Preferences is
MODE_PRIVATE
- the default mode, the created file can only be accessed by the calling application or all applications sharing the same user ID.
- The visitor must know the package name and SharedPreferences name to access via Context, and use the same sharedUserID
- The visitor must know the name and type of every data to retrieve
- All the Shared Apps must use the same sharedUserID in the
AndroidManifest.xml
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
You may need to uninstall the app after you change the sharedUserId.
Define SharedPreferences to Access another App
- We need to match the
PREFERENCE_NAME
andPREFERENCE_PACKAGE
of the another App when accessing
1 | public static int MODE = Context.MODE_PRIVATE; |
Access via Context in the OnCreate Method
Since we are not in the same context, we need to access the context of another app.
- Add this in the
onCreate()
method.
1 | Context c = null; |
Internal and External Storage
The SharedPreferences object enables you to store data that is best stored as name/value pairs
However, sometimes you might prefer to use the traditional file system to store your data
For example, you might want to store the text of poems you want to display in your applications
- In Android, you can use the classes in the java.io package to do so.
- java.io allows Persisting Data to Files.
Internal Storage
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) might need to download images from the web for display.
In this scenario, saving the images to internal storage is a good solution.
You might also need to persist data created by the user, such as when you have an application that enables users to take notes and save them for later use.
Store private data on the device memory, in directory /data/data/<package name>/files
- Cannot be accessed by other apps
- only available on the own app
- When uninstall the app, files will also be removed.
The 2 commonly used functions:
openFileOutput()
openFileInput()
Internal Storage – Save
- To save text into a file, you use the FileOutputStream class.
- The openFileOutput() method opens a named file for writing, with the mode specified
1 | FileOutPutStream fout = openFileOutput("myText.txt", MODE_PRIVATE); |
-
MODE_PRIVATE
constant is to indicate that the created file can only be accessed by the calling application- As long as you use
MODE_PRIVATE
for your files on the internal storage, they are never accessible to other apps - The other mode options have been deprecated since API level 17
- As long as you use
-
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
1 | OutputStreamWriter osw = new OutputStreamWriter(fout); |
An example of Saving data to an internal file:
1 | public void writeData(String str){ |
You can find the
myText.txt
in directory/data/data/<package name>/files
using “Device File Explorer”.
Internal Storage – Read
To read the content of a file:
- Use the FileInputStream class, together with the InputStreamReader class
1 | FileInputStream fin = openFileInput("myText.txt"); |
openFileInput method opens a private file associated with this Context’s application package for reading and it does not require any mode
Because 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
1 | //Assign READ_BLOCK_SIZE = 100 somewhere else |
The read() method of the InputStreamReader object checks the number of characters read and returns –1 if the end of the file is reached
An example of Reading data from an internal file:
1 | public String readData(String fileName){ |
Other Useful APIs
getFilesDir()
- Gets the absolute path to the filesystem directory where your internal files are saved.
getDir()
- Creates (or opens an existing) directory within your internal storage space.
deleteFile()
- Deletes a file saved on the internal storage.
fileList()
- Returns an array of files currently saved by your application.
External Storage
There are times when you need to share your application data with other users
For example, you might create an Android application that logs the coordinates of the locations that a user has been to, and subsequently, you want to share all this data with other users
In this scenario, you can store your files on the SD card of the device so that users can easily transfer the data to other devices (and computers) for use later.
It would be useful to save data to external storage (such as an SD card).
- larger capacity
- larger capability to share the files easily with other users
All files are in the SD card can be:
- globally read
Note we need to check the availabliity using Environment.getExternalStorageState()
- Because External storage can be unmounted by users
External Storage – Getting the path
First we need to find out the directory of the SD card.
- getExternalStorageDirectory() method returns the full path to the external storage
- Typically, it should return the “/sdcard” path for a real device, and “/mnt/sdcard” for an Android emulator
- Never try to hardcode the path of the SD card
- manufacturers may choose to assign a different path name to the SD card
1 | File sdCard = Environnment.getExternalStorageDirectory(); |
External Storage - Save
The idea is same as Internal Storage but this time we save into a SD card.
1 | public void writeData(String str){ |
External Storage - Read
The idea is same as Internal Storage but this time we read from a SD card.
1 | public String readData(String fileName){ |
SQLite database
For saving relational data, using a database is much more efficient
For example, if you want to store the test results of all the students in a school, it is much more efficient to use a database to represent them because you can use database querying to retrieve the results of specific students.
Moreover, using databases enables you to enforce data integrity by specifying the relationships between different sets of data
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
SQLite:
- Self-contained
- Serverless
- Zero-configuration
- Transactional: Atomic, Consistent, Isolated, Durable
- Can run on Windows, Linux, Unix, Mac OS, and embedded OS (e.g., Android, iOS, Palm OS, Symbian, Windows Mobile, etc.)
Create Helper class to encapsulate all the complexities
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.
In order to use the Database, we usually develop an DBAdapter
class.
- A DBAdapter class creates, opens, closes, and uses a SQLite database and hide the implementation from the user.
Before Creating database, we need to declare some constant.
the DATABASE_CREATE
constant contains the SQL statement for creating the contacts table within the MyDB database.
Constructor of DBAdapter class
1 | public DBAdapter(Context ctx) |
The constructor of the DBAdapter class will then create an instance of the DatabaseHelper class to create a new database
Within the DBAdapter class, a private class that extends the SQLiteOpenHelper class is used
- SQLiteOpenHelper is a helper class in Android to manage database creation and version management
- The onCreate() and onUpgrade() methods must be overriden
1 | private static class DatabaseHelper extends SQLiteOpenHelper{ |
1 |
|
Database operation methods
1 | ContentValues initialValues = new ContentValues(); |
In the Insert function, we specify
nullColumnHack
parameter asnull
because SQL doesn’t allow inserting a completely empty row without naming at least one column name. If your provided values is empty, no column names are known and an empty row can’t be inserted.If not set to null, the
nullColumnHack
parameter provides the name of nullable column name to explicitly insert a NULL into in the case where your values is empty.
To Use the Helper Class
Add a contact to the table
1 | //add one contact to the table |
Retrieving all contacts from a table using Cursor
class
1 | //retrieving all contacts from a table |
Retrieving a contact from a table
1 | db.open(); |
Updating a contact in a table
Deleting a contact from a table
Cursor class as return value for queries
Android uses the Cursor class as a return value for queries.
- Think of the Cursor as a pointer to the result set from a database query.
- Using Cursor enables Android to more efficiently manage rows and columns as needed
- You use a ContentValues object to store name/value pairs. Its put() method enables you to insert keys with values of different data types.
Some common functions of Cursor:
moveToFirst()
- Move the cursor to the first entry. This method will return false if the cursor is empty.
moveToNext()
- Move the cursor to the next entry
moveToPrevious()
- Move the cursor to the previous entry
getCount()
- Get the number of rows in the cursor
getColumnIndexOrThrow()
- Returns the zero-based index for the given column name, or throws IllegalArgumentException if the column doesn’t exist
getColumnName()
- Returns the column name at the given zero-based column index
getColumnNames()
- Returns a string array holding the names of all of the columns in the result set
getColumnIndex()
- Returns the zero-based index for the given column name, or -1 if the column doesn’t exist
moveToPosition()
- Move the cursor to an absolute position
getPosition()
- Returns the current position of the cursor in the row set
1 | //Loop through Cursor: |
1 | //Loop through the Cursor (another way): |
Content Provider
One of Android’s 4 basic components
Content provider is the recommended way to share data across packages and applications
- Allow Sharing of Database to other applications.
The Concept of Content Provider
Think of a content provider as a data store
- How it stores its data is not relevant to the application using it
- However, the way in which packages can access the data stored in it using a consistent programming interface is important
A content provider behaves very much like a database
- you can query it, edit its content, and add or delete content
- However, unlike a database, a content provider can use different ways to store its data. The data can be stored in a database, in files, or even over a network
In the previous SQLite part, the application and the database are directly connected by the db object.
However, In Content Provider part , We use Content Provider to connect with the database.
The common Content Providers
Android ships with many useful content providers, including the following:
- Browser
- Stores data such as browser bookmarks, browser history, and so on
- CallLog
- Stores data such as missed calls, call details, and so on
- Contacts
- Stores contact details
- MediaStore
- Stores media files such as audio, video, and images
- Settings
- Stores the device’s settings and preferences
Basic Idea
Data operation functions (insert, delete, update, etc.) are implemented in the ContentProvider class to documents, database or network
- These functions in ContentProvider are called indirectly via the ContentResolver class with URI
- Each data is represented by an REST style URI
Besides the many built-in content providers, you can also create your own content providers
URI (Uniform Resource Identifier)
You can think of URI is a Table like in SQL.
To query a content provider, you specify the query string in the form of a Uniform Resource Identifier (URI), with an optional specifier for a particular row.
- Here’s the format of the query URI:
<standard_prefix>://<authority>/<data_path>/<id>
- standard prefix :
content://
- The standard prefix for content providers is always content://
- authority : specifies the name of the content provider
- e.g.
contacts
for the bulit-in Contacts content provider
- e.g.
- data_path : specifies the kind of data requested
- e.g.
content://contacts/peoeple
- all contacts in the Contacts content provider
- e.g.
- id : specifies the specific record requested
- e.g.
content://contacts/peoeple/2
- contact number 2 in the Contacts content provider
- e.g.
Some examples
most of them are defined as constant under the package
android.provider.*
Using a Content Provider
For Example, we want to retrieve the contacts stored in Contacts application and display them in the ListView.
First we need to declare the URI for accessing the Contacts application(i.e. declare the table we want to access)
1 | Uri allContacts = ContactsContract.Contacts.CONTENT_URI; |
Then we need to check if that app has permission to access the Contacts.
- If there is no permission yet, we need to request permission.
1 | if (ContextCompat.checkSelfPermission(this, |
To access the Contacts application, you need to have the READ_CONTACTS permission in your AndroidManifest.xml
file
1 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" |
Use CursorLoader to perform Cursor query
CursorLoader is an Android API that is used to interact with a ContentProvider asynchronously. By asynchronously I mean, it can do the query in a background thread without blocking the main thread (also called the UI thread). After querying, it will retrieve the result from ContentProvider and reconnect to the Activity on the main thread.
The CursorLoader class performs the cursor query on a background thread and therefore does not block the application UI
CursorLoader is only available beginning with Android API level 11 and later
Firstly, perform the query to get the table with CursorLoader.
1 | Cursor c; |
The SimpleCursorAdapter object maps a cursor to TextViews (or ImageViews) defined in your XML file (activity_main.xml). It maps the data (as represented by columns) to views (as represented by views):
1 | String[] columns = new String[]{ContactsContract.Contacts.DISPLAY_NAME, |
To Perform Different Queries
The CursorLoader parameters:
new CursorLoader( context, URI, projection, selection, selectionArgs, sortOrder);
URI
- FROM tableprojection
- SELECT Columns to Returnselection
- WHERE clauseselectionArgs
- WHERE clause value substitutionsortOrder
- SORT BY
Projection
- the _ID, DISPLAY_NAME, and HAS_PHONE_NUMBER fields are retrieved
1 | String[] projection = new String[]{ContactsContract.Contacts._ID, |
Filtering
- The selection and selectionArgs parameters for the CursorLoader class enable you to specify a SQL WHERE clause to filter the result of the query
selectionArgs
will replace the?
inselection
1 | Cursor c; |
Or we can use the selection
only, selectionArgs
remain null:
1 | Cursor c; |
Sorting
- The sortOrder parameter of the CursorLoader class enables you to specify a SQL ORDER BY clause to sort the result of the query
1 | Cursor c; |
Create your own Content Providers
Creating your own content provider in Android is relatively simple
- Inherit the abstract ContentProvider class
- Override the various methods defined within it
onCreate()
,query()
,getType()
,insert()
,delete()
, andupdate()
- Add description in AndroidManifest.xml
Create Content Provider
Declare CONTENT_URI
and implement UriMatcher
to determine URI for single entry and multiple entries
The “code” is returned when a URI is matched against the given components via the match() function
1 | public void addURI (String authority, String path, int code) |
When using UriMatcher, the match()
function can be called to determine the URI type (e.g., multiple entries or single entry)
1 | switch(uriMatcher.match(uri)){ |
Register Content Provider
- Let the System know you have a provider so other application can use it
- Register Content Provider in AndroidManifest.xml
- Register with the
<provider>
tag
1 | <provider android:name = ".PeopleProvider" |
The above registered a content provider instance called
PeopleProvider
with authorityedu.polyu.peopleprovider
Content Resolver
Content Provider provides an interface to query content.
Content Resolver resolves a URI to a specific Content provider.
The way to query a content provider is
contentResolverInstance.query(URI,.....)
ContentProvider is called via ContentResolver with URI.
1 | ContentResolver resolver = getContentResolver(); |
Content Provider - Query
Once ContentResolver is acquired, the query()
function can be used to obtain the target data set
Below is a query for data ID = 2 (where it is defined in the URI)
1 | ContentResolver resolver = getContentResolver(); |
uri
defines the data set to queryprojection
defines the list of columns to returnselection
andselectionArgs
define the query criteriasortOrder
defines how the rows are sorted
Content Provider - Insert
Use Insert()
to add single data entry or bulkInsert()
to add multiple data entries
Example of Insert()
1 | ContentValues values = new ContentValues(); |
Example of bulkInsert()
1 | ContentValues[] arrayValues = new ContentValues[10]; |
Content Provider - Delete
Use delete()
to erase entries
- To erase a single entry, the data ID can be defined in the URI
- To erase multiple entries, the criteria can be defined in selection
1 | resolver.delete(uri, where, selectionArgs); |
Example: Delete data with ID=2
1 | Uri uri = Uri.parse(CONTENT_URI_STRING + "/" + "2"); |
Example: Delete data with ID>4 with selection
1 | String selection = KEY_ID + ">4"; |
Content Provider - Update
Use update()
to edit entries
- The ID of the data entry to update can be defined in the URI
- Condition to update can be defined in selection (3rd parameter)
1 | resolver.update(uri, values, where, selectionArgs); |
Example: Update data with ID=7
1 | ContentValues values = new ContentValues(); |