Mobile Development · Guide

Local Storage

Where data lives on the device — key-value settings, a real database, files, and the secure store that credentials belong in.

— min read Mobile Development

Four Places, Four Jobs

Storage choice is not a matter of taste: putting a token in preferences or a list of ten thousand rows in a JSON file are both bugs that only show up later — one as a breach, one as a frozen scroll.
StoreForNot for
Key-value preferencesSettings, flags, small primitivesAnything you query, anything secret
DatabaseStructured data you filter, sort or pageLarge binaries
FilesImages, video, documents, cachesAnything needing queries
Secure storeTokens, keys, credentialsBulk data — it is small and slow

On-Device Databases

Both platforms wrap SQLite: Room on Android, Core Data or SwiftData on iOS. You get typed queries, compile-time checking of SQL in Room's case, and observable results — a query that emits again whenever the underlying rows change, which is what makes an offline-first UI live.

@Dao
interface MessageDao {
    // returns a stream: the UI re-renders whenever these rows change
    @Query("SELECT * FROM messages WHERE chatId = :chatId ORDER BY sentAt DESC")
    fun observeChat(chatId: String): Flow<List<Message>>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun upsert(messages: List<Message>)
}

Migrations are not optional. Users update from any previous version, and a schema change without a migration path either crashes on launch or wipes the database. Write the migration with the schema change, and test upgrading from a real old build rather than a fresh install.

Index what you filter and sort on. A query that is instant over the fifty rows on your device is a visible freeze over the twenty thousand a heavy user accumulates.

Key-Value Storage

Every platform has a small key-value store — DataStore on Android (SharedPreferences before it), UserDefaults on iOS. They are for settings and small flags: the chosen theme, whether onboarding was seen, a last-sync timestamp.

They are plain files. Nothing in them is encrypted, they are not designed for size, and reading a large one on the main thread at launch is a classic startup stall — DataStore exists partly to make that read asynchronous.

Never store an auth token, password or key here. On a rooted or jailbroken device these files are readable, and they are frequently included in device backups.

Secure Storage & Keychain

Credentials belong in the platform's hardware-backed store: the Keychain on iOS, the Keystore with EncryptedSharedPreferences or DataStore on Android. Keys are held by secure hardware where available, so the material never sits in your process memory or in a readable file.

PracticeWhy
Short-lived access tokensLimits the value of a stolen one
Refresh token in the secure storeThe one long-lived secret, in the safest place
Biometric gate for sensitive readsThe OS proves the user is present
Exclude from backupsOtherwise the secret leaves the device
Clear on logoutStale credentials are somebody else's session

Assume the device may be compromised. Secure storage raises the cost of extraction considerably; it does not make a token that never expires safe.

Interview Questions

When do you use preferences versus a database?

Preferences for small primitives you read whole — settings, flags, timestamps. A database as soon as you need to query, filter, sort or page, or once the data grows beyond a handful of values.

Why are schema migrations mandatory?

Users upgrade from arbitrary older versions. A schema change without a migration either crashes at launch or silently destroys their data, and you cannot fix it after the fact.

Where do auth tokens belong?

The Keychain on iOS or the Keystore-backed encrypted store on Android — hardware-backed where available. Never in preferences, which are unencrypted files that often end up in backups.

What makes an offline-first UI feel live?

Observable database queries. The screen subscribes to a query rather than fetching once, so any write — including one from a background sync — re-renders it automatically.

Why index local database columns?

Because your test device has fifty rows and a real user has twenty thousand. Filters and sorts without an index turn into visible freezes on exactly the users you care most about.

Why exclude secrets from device backups?

A backup copies data off the device, into a cloud account or a desktop, where the platform's hardware protection no longer applies.

Quick Quiz

1. An auth refresh token belongs in…
2. Shipping a schema change without a migration…
3. Key-value preferences are unsuitable for…
4. Observable database queries let the UI…
5. The main risk of unindexed local queries is…