Mobile application development inevitably faces the need to save information. User settings, image cache, transaction history or offline content all require secure storage right on the device. Question data storage on Android is fundamental to creating quality and responsive products. Choosing the wrong approach can lead to memory leaks, slow performance, or complete loss of information when updating the system.

The Android ecosystem offers developers a wide range of tools to solve these problems. From simple key-value pairs to full-fledged relational databases with support for complex queries. Understanding the differences between SQLite, Room, DataStore and file systems allows the application architect to choose the optimal solution for a specific business case. In this article, we will analyze each method in detail, evaluate their pros and cons, and also look at modern practices for working with local storage.

You should not rely on outdated methods if more secure and asynchronous analogues exist. For example, using global variables to store application state is an anti-pattern that leads to instability when the screen is rotated or the system unloads a process. Proper organization of the data layer ensures that your application will work stably even in the face of aggressive memory management by the operating system.

Simple data and settings: SharedPreferences and DataStore

A mechanism has traditionally been used to store small amounts of primitive data, such as preference flags, authorization tokens, or the last screen opened. SharedPreferences. It is an XML file that stores data in the form of key-value pairs. Working with it is synchronous by default, which can cause interface lag (ANR) when writing large amounts of data in the main thread. However, Google has officially declared it deprecated in favor of the library. The new tool solves the main problems of its predecessor: it is completely asynchronous, uses coroutines (Kotlin Coroutines) or RxJava and does not block the main thread. In addition, DataStore ensures data integrity during failures by first writing changes to a temporary file and then atomically replacing the original.

However, Google has officially announced SharedPreferences deprecated in favor of the library Jetpack DataStore. The new tool solves the main problems of its predecessor: it is completely asynchronous, uses coroutines (Kotlin Coroutines) or RxJava and does not block the main thread. Additionally, DataStore ensures data integrity during failures by first writing changes to a temporary file and then atomically replacing the original.

There are two types of DataStore: Preferences DataStore for storing primitives and Proto DataStore for working with type-safe objects through Protocol Buffers. Migrating to Proto DataStore is recommended for complex preference structures, as it eliminates type cast errors and provides better serialization performance.

โš ๏ธ Attention: Migrating from SharedPreferences to DataStore requires changing the data access architecture. Direct replacement of calls without taking into account asynchrony will lead to compilation errors or logical bugs in runtime.

๐Ÿ“Š What method of storing settings are you using now?
SharedPreferences
DataStore Preferences
DataStore Proto
EncryptedSharedPreferences
Custom JSON format

Relational databases: SQLite and Room abstraction

When an application needs to store structured data with relationships between tables, SQLitecomes to the rescue. This is a lightweight embedded DBMS that is the de facto standard in mobile development. It supports SQL queries, transactions and indexes, allowing you to efficiently manage thousands of records. However, working with bare-bones SQLite requires writing a lot of boilerplate code to create tables, cursors, and mapping results into Java/Kotlin objects.

Library Room from Google is an abstraction layer on top of SQLite. It provides convenient annotations to describe entities (@Entity), data access objects (@Dao) and the database itself (@Database). Room checks SQL queries at the compilation stage, which significantly reduces the number of runtime errors. If you make a typo in the column name in the query, the application will simply not build, rather than crashing for the user.

Using Room simplifies database schema migration. When changing table structures, you must specify a version and provide a migration strategy or allow destructive deletion of data. This is critical for applications that update on users' devices while maintaining their personal information.

  • ๐Ÿ“ฆ Entity: A class representing a table in a database, where each field corresponds to a column.
  • ๐Ÿ” DAO: An interface or abstract class containing methods for accessing data (inserting, deleting, selecting).
  • ๐Ÿ—๏ธ Database: Main class holding the database and serving as an access point to the DAO.
  • ๐Ÿ”„ Migration: A strategy for safely changing the database schema when updating an application version.
๐Ÿ’ก

Always use the .db suffix for the database file name and store it in the application files directory so that the system will automatically clean it up when the application is uninstalled.

Comparison of storage methods: table solutions

The choice of tool depends on the type of data, its volume and security requirements. There is no one-size-fits-all solution that fits all scenarios. Below is a comparative table that helps you decide on a technology for a specific task.

Storage method Data type Structure Performance Security
SharedPreferences Primitives Key-Value Low (synchronous) Plain text
DataStore Primitives/Objects Key-Value/Proto High (asynchronous) Plain text*
Room (SQLite) Complex objects Relational Optimal Encryption (SQLCipher)
File system Binary/Text Arbitrary Depends on size Depends on access rights
Keystore Crypto keys System storage High Hardware protection

It is worth noting that standard storage methods do not provide data encryption out of the box. If you work with user personal data, financial information, or session tokens, you must use additional libraries, such as AndroidX Security or SQLCipher to encrypt the Room database.

โš ๏ธ Attention: Interfaces and methods of Jetpack libraries may be updated. Always check the official Android Developers documentation before introducing new versions of DataStore or Room into production.

Why is Room better than raw SQLite?

Room takes care of all the chores of creating cursors and closing connections. It integrates with LiveData and Flow, allowing you to automatically update the UI when data changes in the database, which is not possible in the standard SQLite API.

Working with files: Internal and External Storage

Sometimes data is impossible or impractical to store in a database. Caching images, downloading large documents, saving logs, or exporting reports to PDF require working with the file system. In Android, there is a strict division into internal (Internal Storage) and external (External Storage) storage.

Internal storage is private to the application and is deleted along with it. This is an ideal place for critical files that should not be accessible to other applications or the user directly. Access to it does not require special permissions in the manifest. External storage (shared device memory) is available to other applications and remains after you delete your application, but requires permission request READ_EXTERNAL_STORAGE or use Storage Access Framework in new versions of Android.

Starting with Android 10 (API 29), the file access model has changed to data-i="103">. Applications no longer have free access to all external memory. Instead, they operate only in their own dedicated directory or request access to specific media files through a system picker. This significantly increases the security of user data. Scoped Storage. Applications no longer have free access to all external memory. Instead, they operate only in their own dedicated directory or request access to specific media files through a system picker. This significantly increases the security of user data.

  • ๐Ÿ“ getFilesDir(): Returns the path to a private directory for permanent files.
  • ๐Ÿ’พ getCacheDir(): Returns the path to a directory for the cache, which the system can clear if there is not enough space.
  • โ˜๏ธ getExternalFilesDir(): Path to files on external media that are deleted when uninstallation.
  • ๐Ÿ”’ Scoped Storage: Limited model for accessing shared files in new OS versions.
๐Ÿ’ก

Always use getCacheDir() to store an image cache. The system will automatically empty this folder if the device needs space, which will prevent memory from becoming full.

Securely storing sensitive data

Storing passwords, PINs, and cryptographic keys in a regular SharedPreferences or text file is a serious security mistake. A rooted attacker or malware can easily extract this data. For such purposes, Android has Android Keystore System.

Keystore allows you to generate and store cryptographic keys in a secure container, often hardware (TEE - Trusted Execution Environment). The keys themselves never leave the secure area; an application can only request a signing or encryption operation by submitting data to the Keystore. This makes retrieving keys almost impossible even if you have full access to the file system.

For convenient work with encrypted preferences, there is a library EncryptedSharedPreferences. It transparently encrypts keys and values โ€‹โ€‹before writing to a file, using the master key from the Android Keystore. This is the best solution for storing authorization tokens and privacy settings without having to write complex cryptographic code manually.

val masterKey = MasterKey.Builder(context)

.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)

.build()

val sharedPreferences = EncryptedSharedPreferences.create(

context,

"secret_shared_prefs",

masterKey,

EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,

EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM

)

โš ๏ธ Warning: Never hardcode encryption keys in the application source code. They can be extracted by decompiling the APK file in a matter of minutes.

โ˜‘๏ธ Data security checklist

Done: 0 / 5

Data migration and version control schemes

The application life cycle implies constant updates. With each release, the database structure may change: new columns are added, tables are renamed, or data types are changed. If these changes are not processed correctly, the application crashes the first time it is launched after the update for old users.

In Room, migration is implemented through a class Migration, where you write SQL commands to transfer the schema from version N to version N+1. For example, the command ALTER TABLE users ADD COLUMN age INTEGER will add a new field without losing existing records. It is important to test each migration step on real data, since an error in the SQL query will lead to the loss of the user base.

An alternative, but dangerous way is to use the strategy fallbackToDestructiveMigration(). It deletes the old database and creates a new one from scratch if the versions do not match. This is only acceptable for caching applications where the loss of local data is not critical. This approach is unacceptable for user-generated content.

How to test migration?

Create a database of the old version on the emulator, fill it with test data, then update the application code to the new version of the scheme and run it. Check that all data is saved and new fields work correctly.

Proper schema versioning requires discipline. Each new function affecting data storage must be accompanied by a corresponding update of the database version and writing a migration script. Ignoring this step turns the application update into a lottery for the user.

FAQ: Frequently asked questions

Where is the SQLite database physically stored on the device?

By default, Room or SQLite database files are located in the directory /data/data/your.package.app/databases/. This folder can only be accessed without root via USB debugging (adb) or for your own application in development mode.

Can Room be used with Kotlin Multiplatform?

The Room library is currently officially supported only for Android. For Kotlin Multiplatform (KMP), it is recommended to use the library SQLDelightwhich provides a similar type-safe approach to working with SQL and supports Android, iOS, Desktop and Web.

What to do if the database is damaged?

Room provides an interface SupportSQLiteDatabase.Callbackmethod onCorruption(). In it you can implement recovery logic: for example, delete a damaged database file and create a new one, losing local data but maintaining the functionality of the application.

How to increase the size of a SQLite database?

SQLite has limits, but they are very large (up to 140 TB). If you are constrained by performance with millions of records, it is worth considering a sharding strategy (dividing into several files) or transferring part of the historical data to cloud storage, leaving only the current cache in the local database.

Do you need to close the database manually?

When using Room and Dependency Injection via Hilt or Koin, the database lifecycle is typically managed by the container. However, in simple scenarios or when using the singleton pattern manually, the database should be closed when the application exits to free up resources, although the OS will do this itself when the process is unloaded.