Local Data Persistence (Room, SQLite, DataStore & Offline-First)
Persist data locally with Room Database, SQLite, DataStore key-value pairs, and design offline-first app architectures.
Module 14: Local Data Persistence (Room, SQLite, DataStore & Offline-First)
Learning Objectives
By the end of this module, you’ll understand:
- Android storage architecture
- Internal vs External storage
- App sandbox
- Files
- SharedPreferences
- Jetpack DataStore
- SQLite
- Relational databases
- SQL basics
- Room ORM
- Entities
- DAOs
- Relationships
- Transactions
- Migrations
- Room + Coroutines + Flow
- Offline-first architecture
- Synchronization strategies
- Caching patterns
- Best practices
Part 1 — Why Do Apps Need Local Storage?
Imagine you’re building a Notes app.
User creates:
Shopping List
Milk
Eggs
Bread
Where is this stored?
If it’s only in memory:
RAM
↓
App Closed
↓
Everything Lost
Bad.
Instead:
Disk Storage
↓
App Closed
↓
Data Still Exists
Persistent storage survives process death, device restarts, and (depending on the storage location) even app updates.
Part 2 — Android Storage Architecture
Android provides several storage options.
Android Storage
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
Preferences Files Databases
│ │ │
DataStore Images/PDFs SQLite/Room
Choosing the right option is critical.
Not every piece of data belongs in a database.
Part 3 — The Android Sandbox
Every Android app runs in its own sandbox.
Conceptually:
Phone
│
├── App A
│ └── Private Files
│
├── App B
│ └── Private Files
│
└── App C
└── Private Files
By default:
- App A cannot directly read App B’s private storage.
- Each app has its own Linux user and private directory.
This isolation is a major security feature.
Internal Storage
Internal storage is private to your app.
Characteristics:
- App-only access
- Removed when the app is uninstalled
- No storage permission required for your own files
Good for:
- User settings
- Databases
- Caches
- Configuration
- Sensitive files
External Storage
Historically, external storage referred to shared device storage (not necessarily an SD card).
Good for:
- Photos
- Videos
- Documents
- User-exported files
Modern Android uses scoped storage, limiting broad filesystem access and encouraging app-specific directories or system pickers.
Part 4 — Files
Sometimes you don’t need a database.
Example:
Resume.pdf
Invoice.pdf
Profile.jpg
These are files.
Store them as files—not database rows.
Databases are optimized for structured data, not large binary assets.
Part 5 — SharedPreferences
For many years Android developers used:
SharedPreferences
Example:
Dark Mode = true
Language = English
LoggedIn = true
Perfect?
Not quite.
Problems:
- Synchronous API (easy to misuse on the main thread)
- Limited type safety
- XML-based storage
- No built-in transactional guarantees
- Can become cumbersome as settings grow
Google now recommends DataStore for new apps.
Part 6 — Jetpack DataStore
DataStore is the modern replacement.
Two variants:
DataStore
│
├── Preferences DataStore
└── Proto DataStore
Preferences DataStore
Stores key-value pairs.
Example:
Theme = Dark
Language = English
Notifications = Enabled
Similar conceptually to SharedPreferences.
Proto DataStore
Stores strongly typed objects using Protocol Buffers.
Example:
UserSettings
↓
Theme
↓
Language
↓
Font Size
Advantages:
- Strong typing
- Schema evolution
- Better maintainability for complex settings
DataStore Characteristics
Unlike SharedPreferences:
- Asynchronous
- Coroutine-friendly
- Exposes data as a
Flow - Transactional updates
- Better error handling
Example flow:
DataStore
↓
Flow
↓
ViewModel
↓
Compose UI
When to Use DataStore
Use DataStore for:
- Theme
- User preferences
- Small configuration values
- Feature flags
- Simple app settings
Don’t use it for thousands of rows of relational data.
Part 7 — SQLite
Android ships with SQLite.
SQLite is:
A lightweight relational database embedded inside the application.
No separate database server is required.
Everything is stored in local database files.
Imagine:
Notes
↓
SQLite Database
↓
Disk
SQLite manages:
- Tables
- Indexes
- Queries
- Transactions
Relational Database
Imagine an e-commerce app.
Users
Orders
Products
Instead of:
Huge File
Data is organized into tables with relationships.
Tables
Example:
Users
| ID | Name |
|---|---|
| 1 | Alice |
| 2 | Bob |
Products
| ID | Name |
|---|---|
| 1 | Laptop |
| 2 | Phone |
Each table stores one type of entity.
Rows
Each record is one row.
Example:
ID = 5
Name = Alice
One user.
Columns
Columns describe attributes.
ID
Name
Age
Email
Every row follows the same structure.
Primary Key
Every table needs a unique identifier.
Example:
ID = 15
No two rows share the same primary key.
Foreign Key
Suppose:
Orders table.
Order
↓
User ID
The User ID references the Users table.
This creates a relationship.
Part 8 — SQL Basics
SQLite uses SQL.
Common operations:
INSERT
Create data.
INSERT INTO users ...
SELECT
Read data.
SELECT * FROM users
UPDATE
Modify data.
UPDATE users
DELETE
Remove data.
DELETE FROM users
These correspond closely to CRUD operations.
Part 9 — Why Room Exists
Using SQLite directly is verbose and error-prone.
Example:
Open Database
↓
Create Query
↓
Execute
↓
Cursor
↓
Convert Objects
↓
Close Cursor
Lots of boilerplate.
Google created Room.
Room
Room is an ORM (Object Relational Mapping) library.
Think:
Kotlin Objects
↓
Room
↓
SQLite
Room maps objects to database rows and generates much of the boilerplate code.
Room Architecture
Entity
↓
DAO
↓
Room
↓
SQLite
These are the three core concepts.
Part 10 — Entity
An Entity represents a database table.
Conceptually:
User
↓
Table
Each property maps to a column.
Example:
User
id
name
email
becomes
| id | name |
|---|
Room handles the mapping.
Primary Key in Room
Every entity should define its primary key.
This uniquely identifies each row and enables updates and deletes.
Part 11 — DAO (Data Access Object)
DAO is where queries live.
Responsibilities:
- Insert
- Update
- Delete
- Query
The DAO abstracts database access from the rest of your app.
Think of it as the database equivalent of a Repository—but at a lower level.
Flow:
Repository
↓
DAO
↓
Room
↓
SQLite
Repositories coordinate data sources; DAOs talk to the database.
Part 12 — Database Class
The database class ties everything together.
Conceptually:
AppDatabase
↓
UserDao
↓
ProductDao
↓
OrderDao
One database can expose multiple DAOs.
Part 13 — Room + Coroutines
Database work can take time.
Never block the Main Thread.
Room integrates naturally with coroutines.
Example flow:
ViewModel
↓
Repository
↓
DAO
↓
Room
↓
SQLite
All executed without freezing the UI.
Part 14 — Room + Flow
One of Room’s most powerful features.
Imagine:
Database
↓
Data Changes
↓
Flow Emits
↓
UI Updates
You don’t need to manually refresh the UI.
Whenever the underlying table changes, Room emits new values through the Flow.
This works exceptionally well with Compose.
Part 15 — Relationships
Real databases rarely have isolated tables.
Example:
User
↓
Orders
One user.
Many orders.
Relationship:
1
↓
Many
One-to-many.
Other common relationships include:
- One-to-one
- Many-to-many
Room supports modeling these relationships with annotations and helper classes.
Part 16 — Transactions
Imagine transferring money.
Withdraw
↓
Deposit
Suppose the app crashes between the two operations.
Money disappears.
Bad.
Transactions guarantee:
Either:
Everything Succeeds
or
Everything Rolls Back
Never a partial update.
This preserves data integrity.
Part 17 — Migrations
Suppose version 1:
User
↓
Name
Later:
Version 2:
User
↓
Name
↓
Email
Existing users already have a database.
What happens?
Room needs a migration.
A migration transforms the old schema into the new one without losing data.
Ignoring migrations can result in crashes or data loss (depending on configuration).
Part 18 — Offline-First Architecture
Modern apps don’t always treat the network as the source of truth for the UI.
Instead:
UI
↓
Room
↓
Repository
↓
Network
The UI reads from the local database.
The Repository synchronizes local and remote data.
Benefits:
- Works offline
- Faster UI
- Consistent data source
- Better user experience
Single Source of Truth
Imagine:
Network
↓
Repository
↓
Room
↓
UI
The UI never reads directly from the network.
It observes Room.
The Repository updates Room after fetching fresh data.
This pattern avoids inconsistencies and simplifies state management.
Part 19 — Synchronization
Suppose:
Phone Offline
User creates:
New Note
Repository:
Save Locally
Later:
Internet Returns
↓
Upload Pending Changes
↓
Mark Synced
This is synchronization.
Many production apps maintain sync queues for pending work.
Conflict Resolution
Imagine:
Phone changes:
Name = Alice
Server changes:
Name = Alicia
Who wins?
Possible strategies:
- Last write wins
- Server wins
- Client wins
- Merge changes
- User resolves conflict
The right strategy depends on your product requirements.
Part 20 — Caching Strategies
Common cache approaches:
Memory Cache
RAM
Very fast.
Lost when the app process dies.
Disk Cache
Room
Files
Persistent.
Slower than memory, but survives restarts.
Network Cache
Managed by HTTP cache headers and libraries like OkHttp.
Reduces unnecessary network traffic.
Complete Data Flow
User opens Products screen.
Compose Screen
│
▼
ViewModel
│
▼
Repository
│
┌────┴────────────┐
▼ ▼
Room Retrofit
│ │
└────────┬────────┘
▼
Updated Database
▼
Flow Emits
▼
Compose Recomposes
The Repository decides when to fetch remote data and when to use local data.
The UI simply observes state.
Common Mistakes
❌ Using DataStore for relational data
DataStore is for preferences and small configuration values—not large datasets.
❌ Storing images in Room
Store image paths or URIs in the database.
Store the actual files separately.
❌ Querying Room on the Main Thread
Always use coroutine-friendly APIs.
❌ Skipping migrations
Schema evolution is inevitable in real apps.
Plan for it.
❌ Letting the UI access DAOs directly
Always go through a Repository to preserve architecture boundaries.
❌ Treating the network as the only source of truth
Offline-first apps generally use the local database as the source observed by the UI.
Mental Model
Imagine a public library.
Reader (UI)
│
▼
Librarian (ViewModel)
│
▼
Library Manager (Repository)
│
┌────┴───────────┐
▼ ▼
Local Shelves Book Supplier
(Room) (Network)
The reader never contacts the book supplier directly.
The librarian doesn’t manage inventory.
Each role has a clear responsibility.
Best Practices
- Use DataStore for preferences.
- Use Room for structured relational data.
- Prefer Flow with Room for reactive updates.
- Keep DAOs focused on database operations.
- Keep synchronization logic inside Repositories.
- Design for offline use whenever practical.
- Test migrations before releasing schema changes.
- Separate files from structured database records.
Interview Questions
- What is the difference between internal and external storage?
- When should you use DataStore instead of Room?
- Why is Room preferred over raw SQLite?
- What is an Entity in Room?
- What is a DAO?
- Explain primary keys and foreign keys.
- What is a database transaction?
- Why are database migrations necessary?
- What does “offline-first” architecture mean?
- What is a Single Source of Truth in Android architecture?
Module 14 Summary
You now understand how Android persists data locally:
- Internal storage securely stores app-private files.
- External storage is suitable for user-accessible content under modern storage rules.
- DataStore replaces SharedPreferences for settings and preferences.
- SQLite is Android’s embedded relational database.
- Room provides a type-safe abstraction over SQLite.
- Entities, DAOs, and Database classes form the core of Room.
- Flow enables reactive database updates.
- Transactions maintain data consistency.
- Migrations safely evolve schemas over time.
- Offline-first architecture improves resilience, speed, and user experience.
At this point, you’ve mastered the two primary data sources used in Android applications:
- Remote data (Retrofit + OkHttp).
- Local data (Room + DataStore).
The Repository sits between them, orchestrating how data flows through your app.
Next Module: Background Work (Services, WorkManager, Foreground Services & Scheduling)
In Module 15, we’ll answer another critical question:
How can an Android app continue working even when the user isn’t actively using it?
We’ll explore:
- Android process lifecycle and background execution limits.
- Services (Started vs Bound).
- Foreground Services and notifications.
- Broadcast Receivers.
- AlarmManager.
- JobScheduler.
- WorkManager architecture and internals.
- Constraints (network, charging, idle, battery).
- One-time vs periodic work.
- Chaining and retrying work.
- Choosing the right background execution API.
This module is essential for building reliable applications that synchronize data, upload files, send notifications, and perform maintenance tasks without compromising battery life or user experience.