Android SharedPreferences Tutorial and Example

Room Database Essentials for Android Developers

Android apps that need to remember user actions between launches rely on persistent storage, and SQLite has long been the workhorse for that job. Room, an abstraction layer Google introduced as part of Jetpack, removes much of the boilerplate that raw SQLite demands while keeping the performance and reliability developers in Sydney, Brisbane and Melbourne have come to expect from native tooling.

The library rests on three building blocks: entities that represent tables, data access objects that expose queries, and a database class that ties everything together. Together they form a clear contract for how data is read, written and evolved inside an Android application, which is particularly useful when the app targets the diverse device landscape found across the Australian market.

Understanding Room and Its Core Components

Room sits on top of SQLite and provides compile-time verification of SQL statements, which catches mistakes before an APK ever reaches the Play Store. For a developer building something like a Melbourne public transport planner or a Brisbane café loyalty program, this verification means fewer crashes tied to malformed queries during morning peak usage.

The library separates responsibilities deliberately. Entities describe the shape of each table, DAOs expose the operations that can be performed on those tables, and the database class holds a single instance of the underlying SQLite connection. Each layer can be tested on its own, which fits the modular patterns many Australian studios favour when handing work between teams in different cities.

Defining Entity Classes

An entity is a plain Kotlin or Java class annotated with @Entity, and each property that becomes a column is annotated with field-level annotations such as @PrimaryKey, @ColumnInfo or @Ignore. A fitness tracking app popular among Sydney runners might model a RunSession with a primary key, a distance in metres, a duration in seconds and a timestamp captured at the local AEST moment.

Primary keys can be auto-generated with @PrimaryKey(autoGenerate = true), which suits incremental records like invoices or health metrics where unique identifiers should not be hand-rolled. For composite identifiers or natural keys, the annotation can simply mark a single field without auto-generation.

Relationships between tables are expressed with @ForeignKey, allowing cascading deletes or restrictions on update behaviour. Embedding one entity inside another is possible through @Embedded, which is handy when the same fields, such as an Australian postcode and state combination, appear together in multiple query results.

Working with Data Access Objects

DAOs are interfaces or abstract classes annotated with @Dao, and Room generates implementations at compile time from the SQL strings attached to each method. A method annotated with @Query("SELECT * FROM RunSession ORDER BY timestamp DESC") returns a list of sessions, while @Insert, @Update and @Delete provide type-safe shortcuts for common operations.

For reactive UIs, a DAO method can return Flow or LiveData so the screen refreshes automatically when the underlying data changes. This pattern is popular among developers building dashboards for local fintech apps, where a balance or transaction list must stay in sync without manual reloads.

Suspending functions let queries run on a background dispatcher, while the @Transaction annotation groups multiple operations into an atomic unit. Developers at Perth-based agencies often appreciate this when handling multi-step updates such as recording a payment and decrementing a wallet in the same call.

Building the Database Class

The database class extends RoomDatabase and is itself annotated with @Database, listing the entity classes it owns and the version number that Room uses for migrations. Singleton instances are usually created through a companion object factory method that ensures only one connection exists per process.

@Database(entities = [RunSession::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun runSessionDao(): RunSessionDao

    companion object {
        @Volatile private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase =
            INSTANCE ?: synchronized(this) {
                INSTANCE ?: Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "run_session.db"
                ).build().also { INSTANCE = it }
            }
    }
}

The example above shows the standard pattern used across many open-source projects shared at meetups in Adelaide and Hobart. Keeping the database file inside the application sandbox prevents other apps from reading it, which matters when the data carries personal information such as medical or financial records.

Migrating and Versioning Room Databases

When the schema changes, a new version number must be declared and a migration supplied. Automatic migrations can be generated by Room when two schema JSON files are provided, while manual migrations give full control through Migration objects passed to addMigrations on the builder.

A common scenario is renaming a column or splitting a single address column into separate street, suburb and postcode fields. Australian postcodes are four digits, so storing them as integers risks truncation and the loss of leading zeros, which is why many teams keep them as strings from the first schema forward.

Fallbacks such as fallbackToDestructiveMigration() are acceptable for early development, yet they erase user data on upgrade, which is rarely suitable for a released app. A pragmatic approach is to combine auto-migrations for safe changes and hand-written migrations for anything that involves data transformation.

Testing and Observing Room Data

Room integrates cleanly with AndroidX test libraries, allowing tests to run against an in-memory database created with Room.inMemoryDatabaseBuilder. That setup is faster than disk-backed variants and resets between tests, which keeps the suite deterministic for engineers working across time zones from Perth to the east coast.

The MigrationTestHelper validates that migrations produce the expected schema, and seed data can be inserted before each test to mirror realistic Australian scenarios, such as pre-loading a list of capital cities or a set of AFL fixtures. Observing queries through Flow makes it easy to assert that a UI consumer sees the correct emissions after an insert or update.

When debugging production issues, the queryCallback and journalMode options on the builder expose internal statements and write-ahead logging behaviour. These tools are particularly helpful when investigating slow queries in analytics-heavy apps, a recurring concern for studios supporting Australian retail clients during end-of-financial-year sales.

Component Purpose Key Annotations Typical Return Types
Entity Represents a table in SQLite @Entity, @PrimaryKey, @ColumnInfo, @Embedded, @ForeignKey Data class instances
DAO Encapsulates queries and operations @Dao, @Query, @Insert, @Update, @Delete, @Transaction Flow, LiveData, suspend, blocking
Database Owns the SQLite connection @Database, extends RoomDatabase Singleton instance
Migration Moves data between versions Migration, AutoMigration Configured via builder
Test Helper Validates schema and queries MigrationTestHelper, inMemoryDatabaseBuilder Test fixtures