Data Storage in Android System

Data storage on a smartphone is a key element that enables the preservation of the current state, settings, and user preferences after closing the application. In short, by using this mechanism, an app’s performance can be streamlined, and the user session can be retained, which is a standard feature in today’s software. There are several options available in Android for data storage, depending on the requirements.

SharedPreferences

SharedPreferences is an API primarily used for storing lightweight data with a simple structure. Data is saved in a file in a key-value format, making SharedPreferences ideal for storing app settings or flags. To save or retrieve data, a reference to the SharedPreferences object must be obtained using the getSharedPreferences method, which requires access to the application context. The method accepts two parameters – the name of the file where the data will be stored and a value that specifies the level of access to the data.

Obtaining a SharedPreferences Object

The following access modes can be defined for SharedPreferences:

  • MODE_PRIVATE: Data saved with this mode is only accessible by the app that saved it.
  • MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE: These modes are deprecated for security reasons. They allowed other apps to read or write data, which could lead to security vulnerabilities.

Instead of these deprecated mechanisms, Android recommends using more secure solutions, such as Content Providers. Content Providers allow data to be shared between applications using specifically defined permissions, providing better access control.

Once access to the SharedPreferences object is obtained, read and write operations can be performed. The SharedPreferences.Editor interface provides methods for saving primitive data types such as strings or booleans. When saving data, the key under which the value will be stored must be specified, so it can be read later. Data retrieval is possible using methods directly on the SharedPreferences object. Below is an example of both operations using this API.

Saving and Reading Data Using SharedPreferences API

Preferences DataStore

Preferences DataStore is a modern technology for data storage that replaces SharedPreferences. Unlike SharedPreferences, DataStore operates asynchronously based on Kotlin Coroutines and Flow, allowing for smoother and more efficient app performance. It supports a key-value model and offers an easy migration path from older solutions. To use Preferences DataStore, the appropriate dependency must be added to the build.gradle file.

Defining the Required Dependency in the .toml File and Adding it to build.gradle

To save data, an instance of DataStore needs to be provided to the repository responsible for managing the data. The instance can be created using the preferencesDataStore delegate, which automatically handles configuration and storage of preferences.

Declaration of the DataStore constant and the implementation of the delegate for obtaining the Preferences DataStore instance

Once the DataStore object is passed to the repository, the edit method can be called to update a value in the preferences, under the previously defined key. The key determines both the name and the type of data that will be stored.

Creating a key for storing string data using the stringPreferencesKey function

Updating the value stored under the TEST_KEY in Preferences DataStore

To read data, you need to use Flow, which will emit the current values of the data. The DataStore object returns the entire preferences object, so the stream must be transformed using the map operator to extract the value associated with the key of interest.

Method to transform the stream of all data from Data Store into a stream containing the value associated with the key of interest

Calling a terminal operation on the stream, which triggers the data collection process

SQLite

SQLite is a relational database designed for storing complex data types. For saving simple app preferences, such as settings or flags, SharedPreferences is more appropriate. However, when you need to store large amounts of information, such as a list of products or user transactions, SQLite is a better choice. Data in SQLite is stored in tables that can be related to each other, allowing for the modelling of more complex structures. Additionally, SQLite supports SQL queries, offering great flexibility in data manipulation.

SQLite is built into the Android system, so no additional dependencies need to be added to the build.gradle file. To perform operations on the database, you need to implement the abstract class SQLiteOpenHelper, which facilitates creating, updating, and managing the database.

Creating the SQLiteHelper class

The primary methods of the class are:

  • onCreate: This method is called only once, when the database is created. It defines the database structure (tables, columns).

Overriding the abstract onCreate method

  • onUpgrade: This method is called when the database is updated (due to a version change). It allows for data migration and database structure updates.

Overriding the abstract onUpgrade method

  • getWritableDatabase: Opens the database in write mode, allowing data to be inserted, updated, or deleted.

Saving a product to the database

  • getReadableDatabase: Opens the database in read-only mode, allowing SELECT operations. This method helps prevent accidental modification of saved data.

Reading products from the database

Calling methods that perform operations on the database

Logcat output

Room

Room is a library that is part of Android Jetpack and simplifies working with SQLite databases. It integrates with LiveData, Coroutines, and Flow to handle asynchronous operations, improving the app’s responsiveness. It is based on classic SQL concepts but also offers features such as automatic mapping of Java/Kotlin objects to database tables (ORM), making it more intuitive to work with data.

To get started with Room, dependencies need to be added to the build.gradle file.

Adding dependencies

Tables are defined by a data class annotated with @Entity. Each variable in this class corresponds to a column in the table.

Example class with @Entity annotation

To provide access to the database, an interface annotated with @Dao is required. There are four key annotations for methods that allow modifying the database:

  • @Insert: Insert data
  • @Update: Update data
  • @Delete: Delete data
  • @Query: Retrieve data – run SQL queries.

Here, you can apply support for asynchronous operations and mark the method as suspend (Coroutines) or return a stream (Flow).

Example interface with @Dao annotation

Next step is to create a class that represents the database. This class must inherit from RoomDatabase and be annotated with @Database. In this class, we define which entities are used and which DAOs are available.

Class representing the database

The Room database is initialized using Room.databaseBuilder, typically in the application class or activity, so the database is accessible throughout the app.

Use example and result:

Calling Room database methods

Output result

Summary

For small datasets, SharedPreferences is sufficient, although it is an older technology that, while still supported, is gradually being replaced by DataStore, especially in new projects. DataStore is actively developed and recommended by Google as a future-proof alternative. For larger datasets, SQLite is more efficient due to the use of indexes, which speed up search and sorting operations. For a more intuitive and modern approach to managing an SQLite database, the Room library is recommended.

Sources:

Code:

Written by
Tomasz Milewski