store data - android authority

Virtually every non-trivial application will have to store data in one way or another. This data can be of different forms, such as user settings, application settings, user data, images, or a cache of data fetched from the internet. Some apps might generate data that ultimately belongs to the user, and so, would prefer to store the data (perhaps documents or media) in a public place that the user can access at anytime, using other apps. Other apps might want to store data, but do not want this data to be read by other apps (or even the user). The Android platform provides developers with multiple ways to store data, with each method having it’s advantages and disadvantages. For this article, we’ll discuss the different data storage techniques available to Android developers, along with sample code to get you started, or to refresh your memory.

Ways to store data

There are basically four different ways to store data in an Android app:

1. Shared Preferences

You should use this to save primitive data in key-value pairs. You have a key, which must be a String, and the corresponding value for that key, which can be one of: boolean, float, int, long or string. Internally, the Android platform stores an app’s Shared Preferences in an xml file in a private directory. An app can have multiple Shared Preferences files. Ideally, you will want to use Shared preferences to store application preferences.

2. Internal Storage

There are lots of situations where you might want to persist data but Shared Preferences is too limiting. You may want to persist Java objects, or images. Or your data logically needs to be persisted using the familiar filesystem hierarchy. The Internal Storage data storage method is specifically for those situations where you need to store data to the device filesystem, but you do not want any other app (even the user) to read this data. Data stored using the Internal Storage method is completely private to your application, and are deleted from the device when your app is uninstalled.

3. External Storage

Conversely, there are other instances where you might want the user to view the files and data saved by your app, if they wish. To save (and/or read) files to the device’s external storage, your app must request for the WRITE_EXTERNAL_STORAGE permission. If you only want to read from the External Storage without writing, request for the READ_EXTERNAL_STORAGE permission. The WRITE_EXTERNAL_STORAGE permission grants both read/write access. However, beginning with Android 4.4, you can actually write to a “private” external storage folder without requesting WRITE_EXTERNAL_STORAGE. The “private” folder can be read by other applications and by the user, however, data stored in these folders are not scanned by the media scanner. This app_private folder is located in the Android/data directory, and is also deleted when your app is uninstalled.

Beginning with Android 7.0, apps can request for access to a particular directory, rather than requesting for access to the entire external storage. This way, your app can, for example, request access to either the pictures directory only, or the documents directory. This is referred to as scoped directory access. For more information about requesting scoped directory access, check out this Android developer tutorial.

4. SQLite database

Finally, Android provides support for apps to use SQLite databases for data storage. Databases created are app specific, and are available to any class within the app, but not to outside applications. It goes without saying, that before you decide to use an SQLite database for data storage in your app, you should have some SQL knowledge.

We’ll discuss each of these in turn. We use data binding techniques for our sample code, and if you are not familiar with this, or need a refresher, check out our previous article on using data binding in android.

Using Shared Preferences

To store data using shared preferences, you must first get a SharedPreferences object. There are two Context methods that can be used to retrieve a SharedPreferences object.

SharedPreferences sharedPreferences = getPreferences(MODE_PRIVATE);

for when your app will have a single preferences file, and

SharedPreferences sharedPreferences = getSharedPreferences(fileNameString, MODE_PRIVATE);

for when your app could have multiple preferences files, or if you prefer to name your SharedPreferences instance.

On getting the SharedPreferences object, you then access it’s Editor using the edit() method. To actually add a value, use the Editor’s putXXX() method, where XXX is one of Boolean, String, Float, Long, Int or StringSet. You can also remove a key-value preference pair with remove().

Finally, make sure to call the Editor’s commit() method after putting or removing values. If you don’t call commit, you changes will not be persisted.

SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(keyString, valueString);
editor.commit();

For our sample app, we allow the user specify a SharedPreferences filename. If the user specifies a name, we request for the SharedPreferences having that name, if not, we request for the default SharedPreference object.

String fileNameString = sharedPreferencesBinding.fileNameEditView.getText().toString();
SharedPreferences sharedPreferences;
if(fileNameString.isEmpty()) {
    sharedPreferences = getPreferences(MODE_PRIVATE);
}
else {
    sharedPreferences = getSharedPreferences(fileNameString, MODE_PRIVATE);
}

store data - shared preferences

Note that there is no method to get a list of all SharedPreferences files stored by your app. If you are going to store more than one SharedPreferences file, you should either have a static list, or be able to get the SharedPreferences name. On the other hand, you could save your SharedPreferences names in the default SharedPreferences file. To store user preferences, consider using a PreferenceActivity or PreferenceFragment, although they both use Shared Preferences to manage the user preference.

Using Internal Storage

The Internal Storage is similar to saving to any other file system. You can get references to File objects, and you can store data of virtually any type using a FileOutputStream. The uniqueness of Internal Storage is simply that it’s contents can be accessed by your app only. To get access to your internal file directory, use the Context getFilesDir() method. To create (or access) a directory within this internal file directory, use the getDir(directoryName, Context.MODE_XXX) method. The getDir() method returns a reference to a File object representing the specified directory, creating it first, if it doesn’t exist.

File directory;
if (filename.isEmpty()) {
    directory = getFilesDir();
}
else {
    directory = getDir(filename, MODE_PRIVATE);
}
File[] files = directory.listFiles();

In the sample above, if the user specified filename is empty, we get the base internal storage directory. If the user specifies a name, we get the named directory, creating first if needed.

To read files, use your preferred file reading method. For our sample, we read the complete file using a Scanner objects. To read a file that’s directly within your internal storage directory (not in any subdirectory), you can use the openFileInput(fileName) method.

FileInputStream fis = openFileInput(filename);
Scanner scanner = new Scanner(fis);
scanner.useDelimiter("\\Z");
String content = scanner.next();
scanner.close();

Similarly, to access a file for writing that’s directly within the Internal Storage directory, use the openFileOutput(fileName) method. To save files, we use the FileOutputStream write.

FileOutputStream fos = openFileOutput(filename, Context.MODE_PRIVATE);
fos.write(internalStorageBinding.saveFileEditText.getText().toString().getBytes());
fos.close();

store data - internal storage

You can see from the image above, that the filepath is in a folder that’s not accessible by the device’s file manager or any other apps (except for rooted devices).

External Storage

Using External Storage is identical to using Internal Storage and other filesystems. The difference here is that external storage devices can be removable drives, and the contents can be read by any and all apps. Since external storage can be removed and/or shared (mounted) with a computer or in one of several other states, before writing to an external storage, your app should check for media availability.

/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

Android has default folders for different types of data, such as PICTURES, MUSIC, RINGTONES and DOCUMENTS among others. For a complete list check out the Environment api. You should endeavor to store data in these directories if possible, depending on the data your app generates. For the snippet below, we save to either DOCUMENTS, PICTURES or MOVIES directory.

    /* Get one of DOCUMENTS, PICTURES or MOVIES
     * There are many others, including RINGTONES, ALARMS, MUSIC, etc which we ignore
     */
    private String getDirectoryType() {
        int checkedButton = externalStorageBinding.fileTypeGroup.getCheckedRadioButtonId();
        if (checkedButton == externalStorageBinding.documentButton.getId())
            return Environment.DIRECTORY_DOCUMENTS;
        if (checkedButton == externalStorageBinding.pictureButton.getId())
            return Environment.DIRECTORY_PICTURES;
        return Environment.DIRECTORY_MOVIES;
    }

Files saved to the external storage can either be saved in a private space, which is deleted when the app is uninstalled, or in a public space, which is accessible to all files, and scanned by the media scanner. In the snippet below, we either get the private folder using getExternalFilesDir(), or the public folder using getExternalStoragePublicDirectory(). The parameter parsed to the method called is the result of the getDirectoryType() method above.

    /* Choose either a private folder (getExternalFilesDir())
     * or public folder (getExternalStoragePublicDirectory())
     */
    private File getTargetFolder() {
        int checkedButton = externalStorageBinding.privateOrPublicGroup.getCheckedRadioButtonId();
        if (checkedButton == externalStorageBinding.privateButton.getId())
            return getExternalFilesDir(getDirectoryType());
        else
            return Environment.getExternalStoragePublicDirectory(getDirectoryType());
    }

store data - external storage

It is entirely possible that some of the default directories have not been created on the user’s device. It is a good idea to call mkdirs() before attempting to create (or save to) the final target file.

File targetFolder = getTargetFolder();
targetFolder.mkdirs();

At this point, you can write to (or read from) the file using your favorite File write/read methods.

SQLite database

Android provides complete support for SQLite databases. The recommended way of creating SQLite databases is to subclass the SQLiteOpenHelper class, and override the onCreate() method. For this sample, we simply create a single table.

public class SampleSQLiteDBHelper extends SQLiteOpenHelper {
    private static final int DATABASE_VERSION = 2;
    public static final String DATABASE_NAME = "sample_database";
    public static final String PERSON_TABLE_NAME = "person";
    public static final String PERSON_COLUMN_ID = "_id";
    public static final String PERSON_COLUMN_NAME = "name";
    public static final String PERSON_COLUMN_AGE = "age";
    public static final String PERSON_COLUMN_GENDER = "gender";

    public SampleSQLiteDBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        sqLiteDatabase.execSQL("CREATE TABLE " + PERSON_TABLE_NAME + " (" +
                PERSON_COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                PERSON_COLUMN_NAME + " TEXT, " +
                PERSON_COLUMN_AGE + " INT UNSIGNED, " +
                PERSON_COLUMN_GENDER + " TEXT" + ")");
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
        sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + PERSON_TABLE_NAME);
        onCreate(sqLiteDatabase);
    }
}

To add data:

    private void saveToDB() {
        SQLiteDatabase database = new SampleSQLiteDBHelper(this).getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(SampleSQLiteDBHelper.PERSON_COLUMN_NAME, activityBinding.nameEditText.getText().toString());
        values.put(SampleSQLiteDBHelper.PERSON_COLUMN_AGE, activityBinding.ageEditText.getText().toString());
        values.put(SampleSQLiteDBHelper.PERSON_COLUMN_GENDER, activityBinding.genderEditText.getText().toString());
        long newRowId = database.insert(SampleSQLiteDBHelper.PERSON_TABLE_NAME, null, values);

        Toast.makeText(this, "The new Row Id is " + newRowId, Toast.LENGTH_LONG).show();
    }

To read data:

    private void readFromDB() {
        String name = activityBinding.nameEditText.getText().toString();
        String gender = activityBinding.genderEditText.getText().toString();
        String age = activityBinding.ageEditText.getText().toString();
        if(age.isEmpty())
            age = "0";

        SQLiteDatabase database = new SampleSQLiteDBHelper(this).getReadableDatabase();

        String[] projection = {
                SampleSQLiteDBHelper.PERSON_COLUMN_ID,
                SampleSQLiteDBHelper.PERSON_COLUMN_NAME,
                SampleSQLiteDBHelper.PERSON_COLUMN_AGE,
                SampleSQLiteDBHelper.PERSON_COLUMN_GENDER
        };

        String selection =
                SampleSQLiteDBHelper.PERSON_COLUMN_NAME + " like ? and " +
                        SampleSQLiteDBHelper.PERSON_COLUMN_AGE + " > ? and " +
                        SampleSQLiteDBHelper.PERSON_COLUMN_GENDER + " like ?";

        String[] selectionArgs = {"%" + name + "%", age, "%" + gender + "%"};

        Cursor cursor = database.query(
                SampleSQLiteDBHelper.PERSON_TABLE_NAME,   // The table to query
                projection,                               // The columns to return
                selection,                                // The columns for the WHERE clause
                selectionArgs,                            // The values for the WHERE clause
                null,                                     // don't group the rows
                null,                                     // don't filter by row groups
                null                                      // don't sort
        );

        Log.d("TAG", "The total cursor count is " + cursor.getCount());
        activityBinding.recycleView.setAdapter(new MyRecyclerViewCursorAdapter(this, cursor));
    }

store data - sqlite

SQLite storage offers the power and speed of a full featured relational database to your app. If you intend to store data that needs to be queried, you should consider using the SQLite storage option. Watch out for our upcoming in-depth article on using sqlite storage in an android app.

Saving Cache Files

Android also provides a means to cache some data, rather than store it permanently. Data can be cached in either internal storage or external storage. Cache files may be deleted by the Android system when the device is low on space.

To get the internal storage cache directory, use the getCacheDir() method. This returns a File object, that represents your app’s internal storage directory. The external cache directory can be accessed with the similarly named getExternalCacheDir().

Although the Android device can delete your cache files if needed, you should not rely on this behavior. Instead, you should maintain the size of your cache files yourself, and always try to keep your cache within a reasonable limit, like the recommended 1MB.

store data - cache

Finally

There are advantages and disadvantages to using each of the different storage methods available. SharedPreferences is the easiest to use, especially if you want to store discrete primitive data types. Internal and external storage is best for storing files such as music, videos and documents, while SQLite wins if you need to perform fast searches and queries on your data. The storage method you choose should ultimately be dependent on your data types, the length of time you need the data, and how private you want the data to be.

As usual, the complete source for the sample app developed above is available on github. Feel free to use as you see fit, and don’t hesitate to reach out with comments, questions and/or tips. Happy coding.

Show 2 comments