Android SharedPreferences Tutorial and Example

Performing CRUD operations with Room in your Android apps

Modern Android apps rely heavily on structured data persistence, and Room has become a favourite among developers across Sydney, Melbourne, and Brisbane. As part of Jetpack, Room abstracts away the SQL boilerplate that once made raw SQLite painful. It also enforces compile-time query validation, so careless typos rarely reach your shipped APK.

For learners on the Android Open Tutorials path, mastering create, read, update, and delete with Room is a foundational skill. The library is opinionated yet flexible, encouraging clean architecture while still allowing hand-crafted SQL. With these four operations mastered, you can wire almost any local store into a RecyclerView, a form, or a background worker.

This walkthrough assumes Kotlin familiarity and a working Android Studio. Whether you are prototyping in Adelaide or shipping production code in Perth, the patterns below transfer across industries and screen sizes.

Setting up Room in your module

Open your build.gradle at the module level and add the Room runtime, the room-ktx helper, and KSP. KSP runs noticeably faster than kapt, which matters once entity counts grow. Re-sync Gradle and confirm the artifacts resolve from Google's maven endpoint before moving on.

Teams behind corporate proxies in Australia sometimes see the initial sync stall. Pointing Gradle directly at the Google maven mirror tends to fix it. Once the build is green, you are ready to define your first entity.

Defining an entity class

An entity is a Kotlin class annotated with @Entity, where each constructor property maps to a database column. Choose a primary key suitable for your domain, whether an auto-generated long or a UUID string. For an app tracking customer visits across a Melbourne cafe chain, a composite key combining shop ID and timestamp may be more meaningful than a simple integer.

Room skips properties that lack annotations or are marked @Ignore, so be deliberate about table content. Type converters handle tricky cases such as Date or custom enums. Treat the entity as your data contract, because changes here ripple through queries, repositories, and UI models.

Building the data access object

The DAO interface is where CRUD lives most cleanly. Annotate it with @Dao and declare one method per operation. Room's code generator produces the implementation at build time, so you never write boilerplate yourself.

Standard annotations include @Insert with OnConflictStrategy.REPLACE, @Query for selective reads, @Update for partial modifications, and @Delete for targeted removal. Returning a Long from an insert captures the new row ID, which is handy when foreign keys need to reference it. For list screens, exposing the table as Flow<List<Entity>> lets the UI react instantly to inserts, updates, and deletions.

Constructing the database class

The database is an abstract class annotated with @Database, listing all entities and a version number. DAOs are exposed as abstract methods, implemented for you by Room. Keep the instance as a singleton through dependency injection or a simple object wrapper.

For offline-first apps used in regional Queensland, robust migration logic matters because users often jump multiple versions ahead. Define migrations with the Migration class, supplying from and to versions plus the SQL needed. fallbackToDestructiveMigration is fine in early development but should never ship to production.

Running the four CRUD operations

Inserting a record is straightforward: build the entity and call the DAO's insert method. Suspend variants let the call sit inside a viewModelScope.launch so the main thread stays responsive.

Reading happens through a Flow-returning query that you collect in a lifecycleScope block. Updates require loading the existing row, mutating fields, and calling the DAO's update method, though some teams prefer a raw @Query("UPDATE ..."). Deletion is similar: locate the record, then call delete or deleteAll. The end-to-end loop is fast, predictable, and easy to test.

Running writes off the main thread

Room refuses to run write operations on the main thread by default. Reads are technically allowed but discouraged for anything beyond trivial sets. Wrapping writes inside coroutines or executors keeps the UI responsive.

A common pattern across Australian tutorials is to expose repository methods as suspend functions and let the ViewModel choose the dispatcher. Use Dispatchers.IO for disk-bound work and Dispatchers.Default for CPU-bound transforms. For heavy migrations, run them inside a WorkManager job so the task survives process death across poor rural coverage.

Putting it all together

Room supports JVM unit tests via Room.inMemoryDatabaseBuilder, giving you a fast way to exercise the DAO without an emulator. Instrumented tests on a device exercise the real SQLite engine, and the database inspector in Android Studio lets you browse rows while the app runs. Pair this with a logging interceptor to inspect the SQL Room generates under the hood.

Just as someone might compare new casino platforms before signing up, exercising your CRUD layer against realistic data shapes before release keeps surprises to a minimum. With the entity, DAO, database class, and thread plan in place, you have a reliable local store ready for your next Android project.

Approach Query validation Async model Migration tooling Learning curve
Room (Jetpack) Compile-time Coroutines and Flow Manual and auto-migrations Moderate
SQLiteOpenHelper Runtime only Manual wrappers Manual SQL only Steep
Realm Partial Native Flow Schema versioning Moderate
ObjectBox Compile-time Native Built-in migration Easy