1. Code
  2. Coding Fundamentals
  3. Databases & SQL

Android From Scratch: How to Store Application Data Locally

Scroll to top
This post is part of a series called Android From Scratch.
Android From Scratch: How to Use Resources In an Application
Android From Scratch: Background Operations

When it comes to persisting application data locally, Android developers are definitely spoiled for choice. In addition to direct access to both the internal and external storage areas of an Android device, the Android platform offers SQLite databases for storing relational data and special files for storing key-value pairs. What's more, Android apps can also use third-party databases that offer NoSQL support.

In this tutorial, I'll show you how to make use of all those storage options in your Android apps. I'll also help you understand how to pick the most appropriate storage option for your data.

1. Storing Key-Value Pairs

If you are looking for a quick way to store a few strings or numbers, you should consider using a preferences file. Android activities and services can use the getDefaultSharedPreferences() method of the PreferenceManager class to get a reference to a SharedPreferences object that can be used to both read from and write to the default preferences file.

1
SharedPreferences myPreferences
2
    = PreferenceManager.getDefaultSharedPreferences(MyActivity.this);

To start writing to the preferences file, you must call the edit() method of the SharedPreferences object, which returns a SharedPreferences.Editor object.

1
SharedPreferences.Editor myEditor = myPreferences.edit();

The SharedPreferences.Editor object has several intuitive methods you can use to store new key-value pairs to the preferences file. For example, you could use the putString() method to put a key-value pair whose value is of type String. Similarly, you could use the putFloat() method to put a key-value pair whose value is of type float. The following code snippet creates three key-value pairs:

1
myEditor.putString("NAME", "Alice");
2
myEditor.putInt("AGE", 25);
3
myEditor.putBoolean("SINGLE?", true);

Once you've added all the pairs, you must call the commit() method of the SharedPreferences.Editor object to make them persist.

1
myEditor.commit();

Reading from a SharedPreferences object is a lot easier. All you need to do is call the appropriate get*() method. For example, to get a key-value pair whose value is of type String, you must call the getString() method. Here's a code snippet that retrieves all the values we added earlier:

1
String name = myPreferences.getString("NAME", "unknown");
2
int age = myPreferences.getInt("AGE", 0);
3
boolean isSingle = myPreferences.getBoolean("SINGLE?", false);

As you can see in the above code, as the second parameter, all the get*() methods expect a default value, which is the value that must be returned if the key is not present in the preferences file.

Note that preferences files are limited to strings and primitive data types only. If you wish to store more complex data types or binary data, you must choose a different storage option.

2. Using DataStore

SharedPreferences have provided developers with an easy way to store key-value pairs for a long time. However, you should now consider using a more modern alternative called DataStore.

There are two implementations of DataStore. The first one is Preferences DataStore, which lets you store and access data using keys. It is simpler to use but doesn't provide type safety. The second one is Proto DataStore, which stores data as instances of a custom data type. It is a little complicated to use but provides type safety.

Let's say you decide to use Preferences DataStore in order to store your key-value data pairs. You will need to use the corresponding key type functions to define the keys for every value that you want to store. Here is an example that stores the user's name, age, and awake status:

1
val USER_NAME = stringPreferencesKey("user_name")
2
val USER_AGE = intPreferencesKey("user_age")
3
val USER_AWAKE = booleanPreferencesKey("user_awake")
4
5
suspend fun saveInformation() {
6
    val inputViewName = findViewById<EditText>(R.id.editName)
7
    val inputViewAge = findViewById<EditText>(R.id.editAge)
8
    val inputAwake = false
9
10
    dataStore.edit {
11
        information ->
12
        information[USER_NAME] = inputViewName.text.toString()
13
        information[USER_AGE] = Integer.parseInt(inputViewAge.text.toString())
14
        information[USER_AWAKE] = inputAwake
15
    }
16
}

The saveInformation() function gets values to save from the EditText views and stores them inside our DataStore. You could call the saveInformation() function on events such as a button click.

When you want to read data from a preference DataStore, you should begin by creating a Flow that can expose the stored value.

1
val userNameFlow: Flow<String> = dataStore.data
2
    .map { information ->
3
        information[USER_NAME] ?: ""
4
    }
5
6
val userAgeFlow: Flow<Int> = dataStore.data
7
    .map { information ->
8
        information[USER_AGE] ?: 0
9
    }
10
11
val userAwakeFlow: Flow<Boolean> = dataStore.data
12
    .map{ information ->
13
        information[USER_AWAKE] ?: false
14
    }

You can read the official Android tutorial to learn more about working with Preferences DataStore.

3. Using an SQLite Database

Every Android app can create and make use of SQLite databases to store large amounts of structured data. As you might already know, SQLite is not only lightweight but also very fast. If you have experience working with relational database management systems and are familiar with both Structured Query Language (SQL) and Java Database Connectivity (JDBC), this might be your preferred storage option.

To create a new SQLite database or to open one that already exists, you can use the openOrCreateDatabase() method inside your activity or service. As its arguments, you must pass the name of your database and the mode in which you want to open it. The most used mode is MODE_PRIVATE, which makes sure that the database is accessible only to your application. For example, here's how you would open or create a database called my.db:

1
SQLiteDatabase myDB = 
2
    openOrCreateDatabase("my.db", MODE_PRIVATE, null);

Once the database has been created, you can use the execSQL() method to run SQL statements on it. The following code shows you how to use the CREATE TABLE SQL statement to create a table called user, which has three columns:

1
myDB.execSQL(
2
    "CREATE TABLE IF NOT EXISTS user (name VARCHAR(200), age INT, is_single INT)"
3
);

Although it's possible to insert new rows into the table using the execSQL() method, it's better to use the insert() method. The insert() method expects a ContentValues object containing the values for each column of the table. A ContentValues object is very similar to a Map object and contains key-value pairs.

Here are two ContentValues objects you can use with the user table:

1
ContentValues row1 = new ContentValues();
2
row1.put("name", "Alice");
3
row1.put("age", 25);
4
row1.put("is_single", 1);
5
6
ContentValues row2 = new ContentValues();
7
row2.put("name", "Bob");
8
row2.put("age", 20);
9
row2.put("is_single", 0);

As you might have guessed, the keys you pass to the put() method must match the names of the columns in the table.

Once your ContentValues objects are ready, you can pass them to the insert() method along with the name of the table.

1
myDB.insert("user", null, row1);
2
myDB.insert("user", null, row2);

To query the database, you can use the rawQuery() method, which returns a Cursor object containing the results of the query.

1
Cursor myCursor = 
2
    myDB.rawQuery("select name, age, is_single from user", null);

A Cursor object can contain zero or more rows. The easiest way to loop through all its rows is to call its moveToNext() method inside a while loop.

To fetch the value of an individual column, you must use methods such as getString() and getInt(), which expect the index of the column. For example, here's how you would retrieve all the values you inserted in the user table:

1
while(myCursor.moveToNext()) {
2
    String name = myCursor.getString(0);
3
    int age = myCursor.getInt(1);
4
    boolean isSingle = (myCursor.getInt(2)) == 1 ? true:false;
5
}

Once you have fetched all the results of your query, make sure that you call the close() method of the Cursor object in order to release all the resources it holds.

1
myCursor.close();

Similarly, when you have finished all your database operations, don't forget to call the close() method of the SQLiteDatabase object.

1
myDB.close();

4. Using the Internal Storage

Every Android app has a private internal storage directory associated with it, in which the app can store text and binary files. Files inside this directory are not accessible to the user or to other apps installed on the user's device. They are also automatically removed when the user uninstalls the app.

Before you can use the internal storage directory, you must determine its location. In order to do so, you can call the getFilesDir() method, which is available in both activities and services.

1
File internalStorageDir = getFilesDir();

To get a reference to a file inside the directory, you can pass the name of the file along with the location you determined. For example, here's how you would get a reference to a file called alice.csv:

1
File alice = new File(internalStorageDir, "alice.csv");

From this point on, you can use your knowledge of Java I/O classes and methods to read from or write to the file. The following code snippet shows you how to use a FileOutputStream object and its write() method to write to the file:

1
// Create file output stream

2
fos = new FileOutputStream(alice);
3
// Write a line to the file

4
fos.write("Alice,25,1".getBytes());
5
// Close the file output stream

6
fos.close();

5. Using the External Storage

Because the internal storage capacity of Android devices is usually fixed and often quite limited, several Android devices support external storage media such as removable micro-SD cards. I recommend that you use this storage option for large files, such as photos and videos.

Unlike internal storage, external storage might not always be available. Therefore, you must always check if it's mounted before using it. To do so, use the getExternalStorageState() method of the Environment class.

1
if(Environment.getExternalStorageState()
2
              .equals(Environment.MEDIA_MOUNTED)) {
3
    // External storage is usable

4
} else {
5
    // External storage is not usable

6
    // Try again later

7
}

Once you are sure that the external storage is available, you can get the path of the external storage directory for your app by calling the getExternalFilesDir() method and passing null as an argument to it. You can then use the path to reference files inside the directory. For example, here's how you would reference a file called bob.jpg in your app's external storage directory:

1
File bob = new File(getExternalFilesDir(null), "bob.jpg");

By asking the user to grant you the WRITE_EXTERNAL_STORAGE permission, you can gain read/write access to the entire file system on the external storage. You can then use well-known public directories to store your photos, movies, and other media files. The Environment class offers a method called getExternalStoragePublicDirectory() to determine the paths of those public directories.

For example, by passing the value Environment.DIRECTORY_PICTURES to the method, you can determine the path of the public directory in which you can store photos. Similarly, if you pass the value Environment.DIRECTORY_MOVIES to the method, you get the path of the public directory in which movies can be stored.

Here's how you would reference a file called bob.jpg in the public pictures directory:

1
File bobInPictures = new File(
2
    Environment.getExternalStoragePublicDirectory(
3
        Environment.DIRECTORY_PICTURES),
4
    "bob.jpg"
5
);

Once you have the File object, you can again use the FileInputStream and FileOutputStream classes to read from or write to it.

While previous versions of Android (prior to Android 11 or API level 30) required you to declare the WRITE_EXTERNAL_STORAGE permission to write to any files outside the app-specific directory, this permission has no effect on newer versions of Android.

You need to use the MANAGE_EXTERNAL_STORAGE permission on Android 11 and above to get write access to files outside the app-specific directory and MediaStore. Apps targeting Android 10 or above also have automatic scoped access to external storage. This means that they can automatically access the app-specific directory on the external storage. You won't need to use the MANAGE_EXTERNAL_STORAGE permission in this case.

Conclusion

You now know how to make the most of the local storage options provided by the Android SDK. Regardless of the storage option you choose, read/write operations can be time-consuming if large amounts of data are involved. Therefore, to make sure that the main UI thread always stays responsive, you must consider running the operations in a different thread.

To learn more about saving application data locally, refer to the official data storage API guide.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.