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.