Mobile application development requires reliable data storage, and this is where the library Roomcomes into the picture. This is an abstract layer on top of SQLite that greatly simplifies working with the database, making the code more predictable and safe. Developers are often faced with the need to store user information locally, and Room is becoming a de facto standard in the Android Jetpack ecosystem.
Unlike raw SQL, using this library allows the compiler to check queries at the project build stage. This means that you will not get an application crash due to a typo in the column name when the user has already downloaded the app. Android Studio will highlight the error immediately, saving hours on debugging.
This article will analyze the architecture of the component in detail, show how to create entities and execute queries. We will also touch on the topic of schema migration, which often raises questions among beginners. Ready to dive into the world of local data storage?
Room Architectural Components
The library is built on three main elements that provide separation of responsibilities in the code. Understanding how they interact is critical to building a scalable application. If you break this structure, maintaining the project in the future will turn into a nightmare.
The first element is Entity. It is a class that maps to a table in the database. Each class instance corresponds to one row in the table. You must annotate this class with a special marker so that Room understands that it is a table.
The second component is DAO (Data Access Object). This is the interface in which you declare methods for working with data: insert, delete, update and select. The compiler automatically generates an implementation of this interface, creating the necessary SQL code.
The third and main element is Database. This is an abstract class that serves as the entry point to the entire database. It contains a list of all entities and schema versions. Through it you access DAO objects.
โ ๏ธ Warning: Never create a database instance in a method onCreate of an activity or fragment. This can lead to blocking of the main thread and freezing of the interface.
Room uses the Repository design pattern, hiding the complexity of working with SQLite behind a simple API.
Creating an Entity
Let's start by defining the data structure. Let's say we're building a notes app. We need to define which fields each note will store. For this, a regular Kotlin or Java class is created.
Each field in the class must have a corresponding column in the table. You can explicitly specify the column name or let the library use the default variable name. Primary key (primary key) is required for each table, it uniquely identifies a record.
Room supports various strategies for generating primary keys. You can use auto-increment so that the database assigns the ID itself, or you can set the values โโmanually. The choice of strategy depends on the logic of your application and synchronization with the server.
- ๐ Use annotation
@PrimaryKey(autoGenerate = true)to automatically number records. - ๐ Annotation
@ColumnInfo(name = "user_name")allows you to set a custom column name in SQL. - ๐ Fields marked as
@Ignorewill not be saved in the database.
It is important to remember about data types. Room supports most primitive types, as well as some classes from the java.util i android.archpackage. If you are using complex objects, you will need a converter.
Setting up the access layer (DAO)
The DAO interface is where all the query logic lives. This is where you write methods that will perform CRUD (Create, Read, Update, Delete) operations. The syntax is intuitive and based on annotations.
Annotation @Insertis used to insert data. You can pass a single object or a list of objects to a method. The library itself will generate the correct SQL query INSERT INTO. This eliminates the routine writing of boilerplate code.
The annotation @Updateis used to update records. It updates rows where the primary key matches the key of the passed object. If there are no matches, the record will not be modified, which prevents accidental errors.
@Daointerface NoteDao {
@Query("SELECT * FROM notes WHERE id = :noteId")
fun getNoteById(noteId: Int): LiveData<Note>
@Insert
fun insertAll(vararg notes: Note)
@Delete
fun delete(note: Note)
}
One โโof the most powerful features is the ability to return data as LiveData or Flow. This allows you to automatically update the application interface when data in the database changes. You don't have to manually call UI update methods.
Database Initialization
To get started, you need to create a database class. It must inherit from an abstract class RoomDatabase. In this class, you declare abstract methods for obtaining DAO instances.
Creating a database instance is a resource-intensive operation. Therefore, it is recommended to use the pattern Singleton. This ensures that only one instance of a database connection exists in the application at any given time.
The class Room.databaseBuilder or Room.inMemoryDatabaseBuilder. The second option creates a database in RAM, which is cleared when the application is closed. This is useful for testing.
| Parameter | Description | Value type |
|---|---|---|
| Context | Application context | Application Context |
| Class | Database class | Class<AppDatabase> |
| Name | Database file name | String |
When building the project, be sure to specify the database name. If you forget to do this, the compiler will throw an error. Here you can also configure the ability to work with the database in the main thread, but this is absolutely not worth doing in production.
โ ๏ธ Attention: Use allowMainThreadQueries() is permissible only for debugging. In a real application, this will cause an exception and crash the system.
Migrating database schemas
Over time, application requirements change, and you will need to add new fields to tables or create new entities. A simple class change Entity without updating the database version will cause the application to crash for users when updating.
Room requires an explicit specification of the migration strategy. You must create an object Migrationthat describes how to migrate the database from version 1 to version 2. Inside this object you write SQL commands ALTER TABLE.
If you do not want to write SQL manually, you can use destructive migration. This means deleting the old database and creating a new one from scratch. All user data will be irretrievably lostso use this method only for test applications or if the data is not critical.
How does destructive migration work?
When used fallbackToDestructiveMigration() Room will simply delete the database file when a schema incompatibility is detected and create a clean database again.
The process of adding a migration is as follows: you create a migration object, define a version range, and write SQL queries. This object is then passed to the database builder.
val MIGRATION_1_2 = object : Migration(1, 2) {override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE notes ADD COLUMN priority INTEGER NOT NULL DEFAULT 0")
}
}
You canโt forget about migrations. If the schema version in the code is higher than the file version on the user's device and the migration is not found, the application will fail with an error IllegalStateException. Always test updates on clean installations of different versions.
Data types and converters
SQLite supports a limited set of data types: NULL, INTEGER, REAL, TEXT and BLOB. What to do if you want to save an object Date, Uri or a complex custom class in the database? There are converters for this.
A converter is a class with methods that convert a complex type into one understandable by SQLite and back. For example, a date can be turned into a Long (timestamp), and a list of strings into a JSON string.
To connect the converter, use the annotation @TypeConverters. It can be applied to the entire database, to a specific DAO, or even to a single entity field. This gives flexibility in managing data types.
- ๐ Convert
DatetoLongto store timestamps. - ๐ Convert
List<String>to a string using a delimiter or JSON. - ๐ Store objects
Bitmapas byte arrays (although this is not recommended for large images).
Using JSON to store complex objects is convenient, but it removes the ability to make selections on the internal fields of that object via SQL. If you need to filter data inside a nested object, it is better to normalize the base and put it in a separate table.
To work with date and time, use ready-made converters from the room-ktx library or write your own using System.currentTimeMillis().
Testing and debugging
Like any other the application component, the database, needs to be tested. Room provides excellent tools for unit testing DAOs. You can run the database in memory and check the correctness of queries without creating files on disk.
When writing tests, use annotation @RunWith(AndroidJUnit4::class). Create a database via Room.inMemoryDatabaseBuilder. This ensures that each test starts from a clean state and does not affect other tests.
For debugging a real application, it is convenient to use the tool Database Inspector in Android Studio. It allows you to view tables, edit data in real time and execute SQL queries directly while the application is running on an emulator or device.
โ ๏ธ Attention: The Database Inspector interface may display data incorrectly if you use custom converters. Always check the raw data in the table.
Don't forget to check query performance. Complex JOIN operations or fetching huge lists can slow down your application. Use the keyword EXPLAIN in SQL to analyze the query execution plan.
โ๏ธ Checklist before database release
Frequently asked questions (FAQ)
Can I use Room without Kotlin?
Yes, the library fully supports Java. The annotation syntax is identical, but the implementation may require a little more code due to the lack of language extensions.
How to increase the size of a database if it has reached the limit?
SQLite has a theoretical limit of 140 TB, but in practice the limits are imposed by the device file system. If your database is growing too quickly, consider archiving old data or using pagination.
Is it safe to store passwords in Room?
No, Room does not encrypt data by default. To store sensitive information, use EncryptedSharedPreferences or a library AndroidX Security together with custom converters to encrypt fields.
What to do if you forgot to add a migration?
If the application has already been released, you will have to release an update with the correct migration. For users who have not yet updated, you can use destructive migration in the next version, but they will lose data.