Mobile Data Persistence
You are a senior mobile engineer. Help the user choose, design, or review local data persistence strategies with platform-specific guidance.
Process
Step 1: Understand Storage Requirements
| Question |
Why It Matters |
| What kind of data? (settings, cached API data, user-generated content, auth tokens) |
Determines storage type |
| How much data? (KB, MB, GB) |
Determines storage engine |
| How is data queried? (key lookup, filtered lists, full-text search, joins) |
Determines SQL vs. NoSQL vs. key-value |
| Does data need to survive app uninstall? |
Cloud backup vs. local-only |
| Is data sensitive? (tokens, PII, financial) |
Requires secure/encrypted storage |
| Does data need to sync with a server? |
Sync and conflict resolution strategy |
| What is the read/write ratio? |
Optimization focus |
Step 2: Choose Storage Type
| Type |
Use Case |
Examples |
Max Size |
| Key-value |
Settings, flags, small preferences |
SharedPreferences, UserDefaults, DataStore |
~1 MB |
| SQL database |
Structured data, relational queries, large datasets |
Room, Core Data, Drift, SQLite |
GBs |
| NoSQL / document |
Flexible schemas, fast writes, object storage |
Hive, Isar, Realm, SwiftData |
GBs |
| File storage |
Images, documents, large blobs, exports |
File system, app sandbox |
Device limit |
| Secure storage |
Auth tokens, API keys, passwords, biometric data |
Keychain, EncryptedSharedPreferences, flutter_secure_storage |
KBs |
| In-memory cache |
Temporary data, session state, computed values |
LRU cache, HashMap, NSCache |
~100 MB |
Step 3: Apply Platform-Specific Solutions
Flutter
| Solution |
Type |
Best For |
Async |
Reactive |
| shared_preferences |
Key-value |
Simple settings, flags, small strings |
Yes |
No |
| Hive |
NoSQL (binary) |
Fast read/write, type adapters, no native deps |
Yes |
Via Box listeners |
| Isar |
NoSQL (embedded) |
Large datasets, full-text search, complex queries |
Yes |
Yes (watch queries) |
| Drift (moor) |
SQL (SQLite) |
Relational data, complex queries, migrations |
Yes |
Yes (streams) |
| sqflite |
SQL (SQLite) |
Raw SQL, lightweight, no codegen |
Yes |
No |
| ObjectBox |
NoSQL (object) |
Fast CRUD, relations, sync-capable |
Yes |
Yes (streams) |
| flutter_secure_storage |
Secure key-value |
Auth tokens, API keys, secrets |
Yes |
No |
| path_provider + File |
File |
Images, documents, exports |
Yes |
No |
| hydrated_bloc |
State persistence |
Auto-persist BLoC state across restarts |
Yes |
Yes |
// shared_preferences — simple settings
final prefs = await SharedPreferences.getInstance();
await prefs.setString('theme', 'dark');
final theme = prefs.getString('theme') ?? 'light';
// Drift — reactive SQL database
@DriftDatabase(tables: [Products, Orders])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
Stream<List<Product>> watchAllProducts() =>
select(products).watch();
Future<void> insertProduct(ProductsCompanion product) =>
into(products).insert(product);
}
// Hive — NoSQL with type adapters
@HiveType(typeId: 0)
class Product extends HiveObject {
@HiveField(0)
late String name;
@HiveField(1)
late double price;
}
final box = await Hive.openBox<Product>('products');
box.put('item1', Product()..name = 'Widget'..price = 9.99);
// flutter_secure_storage — secrets
final storage = FlutterSecureStorage();
await storage.write(key: 'auth_token', value: token);
final token = await storage.read(key: 'auth_token');
// Isar — full-text search
@collection
class Product {
Id id = Isar.autoIncrement;
@Index(type: IndexType.value)
late String name;
late double price;
}
final products = await isar.products
.filter()
.nameContains('widget')
.sortByPrice()
.findAll();
Android (Kotlin & Java)
| Solution |
Type |
Best For |
Language |
Reactive |
| DataStore (Preferences) |
Key-value |
Settings, flags (modern replacement for SharedPrefs) |
Kotlin |
Yes (Flow) |
| DataStore (Proto) |
Typed key-value |
Structured settings with schema |
Kotlin |
Yes (Flow) |
| SharedPreferences |
Key-value |
Simple settings (legacy, still widely used) |
Java/Kotlin |
No (but listeners exist) |
| Room |
SQL (SQLite) |
Relational data, complex queries, migrations |
Java/Kotlin |
Yes (Flow/LiveData/RxJava) |
| SQLiteOpenHelper |
SQL (raw) |
Direct SQLite access (legacy) |
Java |
No |
| Realm |
NoSQL (object) |
Fast CRUD, cross-platform sync |
Java/Kotlin |
Yes |
| EncryptedSharedPreferences |
Secure key-value |
Tokens, sensitive settings |
Java/Kotlin |
No |
| Android Keystore |
Secure hardware |
Cryptographic keys, biometric-gated secrets |
Java/Kotlin |
No |
| File (internal/external) |
File |
Images, documents, exports, cache |
Java/Kotlin |
No |
// DataStore — modern key-value (Kotlin)
val THEME_KEY = stringPreferencesKey("theme")
val themeFlow: Flow<String> = dataStore.data.map { prefs ->
prefs[THEME_KEY] ?: "light"
}
suspend fun setTheme(theme: String) {
dataStore.edit { prefs -> prefs[THEME_KEY] = theme }
}
// Room — reactive SQL database
@Entity
data class Product(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val price: Double
)
@Dao
interface ProductDao {
@Query("SELECT * FROM product ORDER BY name")
fun watchAll(): Flow<List<Product>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(product: Product)
}
// EncryptedSharedPreferences — secure storage
val securePrefs = EncryptedSharedPreferences.create(
"secure_prefs",
masterKey,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
securePrefs.edit().putString("auth_token", token).apply()
// SharedPreferences — legacy Java approach
SharedPreferences prefs = getSharedPreferences("settings", MODE_PRIVATE);
prefs.edit().putString("theme", "dark").apply();
String theme = prefs.getString("theme", "light");
// Room — Java with LiveData
@Dao
public interface ProductDao {
@Query("SELECT * FROM product ORDER BY name")
LiveData<List<Product>> watchAll();
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(Product product);
}
// SQLiteOpenHelper — raw legacy approach
public class DbHelper extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE product (id INTEGER PRIMARY KEY, name TEXT, price REAL)");
}
}
iOS (Swift & Objective-C)
| Solution |
Type |
Best For |
Language |
Reactive |
| SwiftData |
ORM (SQLite) |
Modern persistence, SwiftUI integration (iOS 17+) |
Swift |
Yes (@Query) |
| Core Data |
ORM (SQLite) |
Complex object graphs, migrations, iCloud sync |
Swift/ObjC |
Yes (NSFetchedResultsController) |
| UserDefaults |
Key-value (plist) |
Simple settings, flags, small values |
Swift/ObjC |
No (but KVO works) |
| Keychain Services |
Secure storage |
Auth tokens, passwords, certificates, API keys |
Swift/ObjC |
No |
| SQLite (direct) |
SQL (raw) |
Direct SQL, lightweight, GRDB.swift wrapper |
Swift/ObjC |
GRDB: Yes |
| Realm |
NoSQL (object) |
Fast CRUD, cross-platform, real-time sync |
Swift/ObjC |
Yes |
| File Manager |
File |
Documents, images, exports, cache |
Swift/ObjC |
No |
| NSUbiquitousKeyValueStore |
Key-value (iCloud) |
Small settings synced via iCloud |
Swift/ObjC |
No |
| CloudKit |
Cloud database |
User-generated content with iCloud sync |
Swift |
Yes |
| PropertyListSerialization |
Plist file |
Structured settings, legacy config |
ObjC/Swift |
No |
| NSCoding / NSKeyedArchiver |
Binary archive |
Object serialization (legacy) |
ObjC/Swift |
No |
// SwiftData (iOS 17+) — modern approach
@Model
class Product {
var name: String
var price: Double
var createdAt: Date
init(name: String, price: Double) {
self.name = name
self.price = price
self.createdAt = .now
}
}
// In SwiftUI View
@Query(sort: \Product.name) var products: [Product]
// Insert
modelContext.insert(Product(name: "Widget", price: 9.99))
// UserDefaults — simple settings
UserDefaults.standard.set("dark", forKey: "theme")
let theme = UserDefaults.standard.string(forKey: "theme") ?? "light"
// Keychain — secure storage (using wrapper)
let keychain = Keychain(service: "com.myapp")
try keychain.set(token, key: "auth_token")
let token = try keychain.get("auth_token")
// Core Data — Swift
let fetchRequest: NSFetchRequest<Product> = Product.fetchRequest()
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Product.name, ascending: true)]
let controller = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: context,
sectionNameKeyPath: nil,
cacheName: nil
)
// NSUserDefaults — Objective-C
[[NSUserDefaults standardUserDefaults] setObject:@"dark" forKey:@"theme"];
NSString *theme = [[NSUserDefaults standardUserDefaults] stringForKey:@"theme"] ?: @"light";
// Core Data — Objective-C
NSFetchRequest *request = [Product fetchRequest];
request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]];
NSArray<Product *> *products = [context executeFetchRequest:request error:nil];
// NSKeyedArchiver — legacy object persistence
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:products
requiringSecureCoding:YES
error:nil];
[data writeToFile:path atomically:YES];
// Keychain — Objective-C (Security framework)
NSDictionary *query = @{
(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
(__bridge id)kSecAttrAccount: @"auth_token",
(__bridge id)kSecValueData: [token dataUsingEncoding:NSUTF8StringEncoding]
};
SecItemAdd((__bridge CFDictionaryRef)query, NULL);
Step 4: Design the Data Layer
Repository pattern (recommended for all platforms):
UI ← ViewModel/BLoC ← Repository ← LocalDataSource (Room/CoreData/Drift)
← RemoteDataSource (API)
| Strategy |
Description |
When to Use |
| Cache-aside |
App checks cache, fetches from network on miss |
Simple caching, read-heavy |
| Write-through |
Write to cache and network simultaneously |
Data consistency critical |
| Write-behind |
Write to cache immediately, sync to network async |
Offline-first, write-heavy |
| Refresh-ahead |
Proactively refresh cache before expiry |
Predictable access patterns |
Step 5: Handle Migrations
| Platform |
Migration Tool |
Strategy |
| Flutter (Drift) |
Schema versioning + migration steps |
Forward-only SQL migrations |
| Flutter (Hive/Isar) |
Type adapter versioning |
Schema-less, handle missing fields |
| Android (Room) |
Migration(fromVersion, toVersion) |
SQL ALTER statements |
| iOS (Core Data) |
Lightweight migration (automatic) or mapping models |
Prefer lightweight when possible |
| iOS (SwiftData) |
VersionedSchema + SchemaMigrationPlan |
Declarative migration stages |
Migration rules:
- Never delete columns in production — deprecate and ignore
- Always test migrations with real production-like data
- Support skipping versions (1 → 3, not just 1 → 2 → 3)
- Back up database before destructive migrations
Step 6: Secure Sensitive Data
| Data Type |
Storage |
Platform Implementation |
| Auth tokens |
Secure storage |
Keychain (iOS), EncryptedSharedPrefs (Android), flutter_secure_storage |
| API keys |
Secure storage or build config |
Never hardcode in source |
| User PII |
Encrypted database |
SQLCipher, encrypted Room, encrypted Core Data |
| Biometric-gated data |
Hardware-backed secure storage |
Keychain + biometric policy (iOS), Keystore + BiometricPrompt (Android) |
| Session data |
Memory only |
Never persist session tokens to disk unencrypted |
Output Format
## Persistence Summary
- **Platform:** [Flutter / Android / iOS]
- **Data types:** [What data needs to be stored]
- **Solutions chosen:** [key-value, SQL, NoSQL, secure storage]
## Storage Architecture
[Data flow diagram: UI → Repository → Local/Remote sources]
## Schema Design
[Core entities, relationships, indexes]
## Migration Strategy
[How schema changes are handled]
## Security
[How sensitive data is protected]
## Sync Strategy (if applicable)
[How local data syncs with server]
Quality Checklist
Edge Cases
- SharedPreferences/UserDefaults are NOT encrypted — never store tokens or PII in them
- Core Data + iCloud sync has many edge cases (conflict resolution, account switching) — test thoroughly
- Room migrations that fail will destroy the database by default — always provide migration paths or use
fallbackToDestructiveMigration() only in dev
- On Android,
MODE_WORLD_READABLE SharedPreferences is deprecated and insecure — always use MODE_PRIVATE
- iOS Keychain items persist across app reinstalls by default — set
kSecAttrAccessible appropriately
- For large binary files (images, videos), store the file on disk and keep only the path/reference in the database
- SQLite has a practical limit of ~1GB per database on mobile — shard or archive old data for larger datasets
1---2name: mobile-data-persistence3description: Choose, design, and review local data storage in mobile apps — key-value stores, SQL databases, NoSQL, file storage, secure storage, and sync strategies across Flutter (Hive, Isar, Drift, SharedPreferences), Android (Room, DataStore, SharedPreferences), and iOS (Core Data, SwiftData, UserDefaults, Keychain). TRIGGER when: user says /mobile-data-persistence, asks about local storage in a mobile app, needs to choose a persistence solution, or wants to review data layer design.4---56# Mobile Data Persistence78You are a senior mobile engineer. Help the user choose, design, or review local data persistence strategies with platform-specific guidance.910## Process1112### Step 1: Understand Storage Requirements1314| Question | Why It Matters |15|----------|---------------|16| What kind of data? (settings, cached API data, user-generated content, auth tokens) | Determines storage type |17| How much data? (KB, MB, GB) | Determines storage engine |18| How is data queried? (key lookup, filtered lists, full-text search, joins) | Determines SQL vs. NoSQL vs. key-value |19| Does data need to survive app uninstall? | Cloud backup vs. local-only |20| Is data sensitive? (tokens, PII, financial) | Requires secure/encrypted storage |21| Does data need to sync with a server? | Sync and conflict resolution strategy |22| What is the read/write ratio? | Optimization focus |2324### Step 2: Choose Storage Type2526| Type | Use Case | Examples | Max Size |27|------|----------|---------|----------|28| **Key-value** | Settings, flags, small preferences | SharedPreferences, UserDefaults, DataStore | ~1 MB |29| **SQL database** | Structured data, relational queries, large datasets | Room, Core Data, Drift, SQLite | GBs |30| **NoSQL / document** | Flexible schemas, fast writes, object storage | Hive, Isar, Realm, SwiftData | GBs |31| **File storage** | Images, documents, large blobs, exports | File system, app sandbox | Device limit |32| **Secure storage** | Auth tokens, API keys, passwords, biometric data | Keychain, EncryptedSharedPreferences, flutter_secure_storage | KBs |33| **In-memory cache** | Temporary data, session state, computed values | LRU cache, HashMap, NSCache | ~100 MB |3435### Step 3: Apply Platform-Specific Solutions3637#### Flutter3839| Solution | Type | Best For | Async | Reactive |40|----------|------|----------|-------|----------|41| **shared_preferences** | Key-value | Simple settings, flags, small strings | Yes | No |42| **Hive** | NoSQL (binary) | Fast read/write, type adapters, no native deps | Yes | Via Box listeners |43| **Isar** | NoSQL (embedded) | Large datasets, full-text search, complex queries | Yes | Yes (watch queries) |44| **Drift (moor)** | SQL (SQLite) | Relational data, complex queries, migrations | Yes | Yes (streams) |45| **sqflite** | SQL (SQLite) | Raw SQL, lightweight, no codegen | Yes | No |46| **ObjectBox** | NoSQL (object) | Fast CRUD, relations, sync-capable | Yes | Yes (streams) |47| **flutter_secure_storage** | Secure key-value | Auth tokens, API keys, secrets | Yes | No |48| **path_provider + File** | File | Images, documents, exports | Yes | No |49| **hydrated_bloc** | State persistence | Auto-persist BLoC state across restarts | Yes | Yes |5051```dart52// shared_preferences — simple settings53final prefs = await SharedPreferences.getInstance();54await prefs.setString('theme', 'dark');55final theme = prefs.getString('theme') ?? 'light';5657// Drift — reactive SQL database58@DriftDatabase(tables: [Products, Orders])59class AppDatabase extends _$AppDatabase {60 AppDatabase() : super(_openConnection());6162 Stream<List<Product>> watchAllProducts() =>63 select(products).watch();6465 Future<void> insertProduct(ProductsCompanion product) =>66 into(products).insert(product);67}6869// Hive — NoSQL with type adapters70@HiveType(typeId: 0)71class Product extends HiveObject {72 @HiveField(0)73 late String name;74 @HiveField(1)75 late double price;76}7778final box = await Hive.openBox<Product>('products');79box.put('item1', Product()..name = 'Widget'..price = 9.99);8081// flutter_secure_storage — secrets82final storage = FlutterSecureStorage();83await storage.write(key: 'auth_token', value: token);84final token = await storage.read(key: 'auth_token');8586// Isar — full-text search87@collection88class Product {89 Id id = Isar.autoIncrement;90 @Index(type: IndexType.value)91 late String name;92 late double price;93}9495final products = await isar.products96 .filter()97 .nameContains('widget')98 .sortByPrice()99 .findAll();100```101102#### Android (Kotlin & Java)103104| Solution | Type | Best For | Language | Reactive |105|----------|------|----------|----------|----------|106| **DataStore (Preferences)** | Key-value | Settings, flags (modern replacement for SharedPrefs) | Kotlin | Yes (Flow) |107| **DataStore (Proto)** | Typed key-value | Structured settings with schema | Kotlin | Yes (Flow) |108| **SharedPreferences** | Key-value | Simple settings (legacy, still widely used) | Java/Kotlin | No (but listeners exist) |109| **Room** | SQL (SQLite) | Relational data, complex queries, migrations | Java/Kotlin | Yes (Flow/LiveData/RxJava) |110| **SQLiteOpenHelper** | SQL (raw) | Direct SQLite access (legacy) | Java | No |111| **Realm** | NoSQL (object) | Fast CRUD, cross-platform sync | Java/Kotlin | Yes |112| **EncryptedSharedPreferences** | Secure key-value | Tokens, sensitive settings | Java/Kotlin | No |113| **Android Keystore** | Secure hardware | Cryptographic keys, biometric-gated secrets | Java/Kotlin | No |114| **File (internal/external)** | File | Images, documents, exports, cache | Java/Kotlin | No |115116```kotlin117// DataStore — modern key-value (Kotlin)118val THEME_KEY = stringPreferencesKey("theme")119120val themeFlow: Flow<String> = dataStore.data.map { prefs ->121 prefs[THEME_KEY] ?: "light"122}123124suspend fun setTheme(theme: String) {125 dataStore.edit { prefs -> prefs[THEME_KEY] = theme }126}127128// Room — reactive SQL database129@Entity130data class Product(131 @PrimaryKey(autoGenerate = true) val id: Long = 0,132 val name: String,133 val price: Double134)135136@Dao137interface ProductDao {138 @Query("SELECT * FROM product ORDER BY name")139 fun watchAll(): Flow<List<Product>>140141 @Insert(onConflict = OnConflictStrategy.REPLACE)142 suspend fun insert(product: Product)143}144145// EncryptedSharedPreferences — secure storage146val securePrefs = EncryptedSharedPreferences.create(147 "secure_prefs",148 masterKey,149 context,150 EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,151 EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM152)153securePrefs.edit().putString("auth_token", token).apply()154```155156```java157// SharedPreferences — legacy Java approach158SharedPreferences prefs = getSharedPreferences("settings", MODE_PRIVATE);159prefs.edit().putString("theme", "dark").apply();160String theme = prefs.getString("theme", "light");161162// Room — Java with LiveData163@Dao164public interface ProductDao {165 @Query("SELECT * FROM product ORDER BY name")166 LiveData<List<Product>> watchAll();167168 @Insert(onConflict = OnConflictStrategy.REPLACE)169 void insert(Product product);170}171172// SQLiteOpenHelper — raw legacy approach173public class DbHelper extends SQLiteOpenHelper {174 @Override175 public void onCreate(SQLiteDatabase db) {176 db.execSQL("CREATE TABLE product (id INTEGER PRIMARY KEY, name TEXT, price REAL)");177 }178}179```180181#### iOS (Swift & Objective-C)182183| Solution | Type | Best For | Language | Reactive |184|----------|------|----------|----------|----------|185| **SwiftData** | ORM (SQLite) | Modern persistence, SwiftUI integration (iOS 17+) | Swift | Yes (@Query) |186| **Core Data** | ORM (SQLite) | Complex object graphs, migrations, iCloud sync | Swift/ObjC | Yes (NSFetchedResultsController) |187| **UserDefaults** | Key-value (plist) | Simple settings, flags, small values | Swift/ObjC | No (but KVO works) |188| **Keychain Services** | Secure storage | Auth tokens, passwords, certificates, API keys | Swift/ObjC | No |189| **SQLite (direct)** | SQL (raw) | Direct SQL, lightweight, GRDB.swift wrapper | Swift/ObjC | GRDB: Yes |190| **Realm** | NoSQL (object) | Fast CRUD, cross-platform, real-time sync | Swift/ObjC | Yes |191| **File Manager** | File | Documents, images, exports, cache | Swift/ObjC | No |192| **NSUbiquitousKeyValueStore** | Key-value (iCloud) | Small settings synced via iCloud | Swift/ObjC | No |193| **CloudKit** | Cloud database | User-generated content with iCloud sync | Swift | Yes |194| **PropertyListSerialization** | Plist file | Structured settings, legacy config | ObjC/Swift | No |195| **NSCoding / NSKeyedArchiver** | Binary archive | Object serialization (legacy) | ObjC/Swift | No |196197```swift198// SwiftData (iOS 17+) — modern approach199@Model200class Product {201 var name: String202 var price: Double203 var createdAt: Date204205 init(name: String, price: Double) {206 self.name = name207 self.price = price208 self.createdAt = .now209 }210}211212// In SwiftUI View213@Query(sort: \Product.name) var products: [Product]214215// Insert216modelContext.insert(Product(name: "Widget", price: 9.99))217218// UserDefaults — simple settings219UserDefaults.standard.set("dark", forKey: "theme")220let theme = UserDefaults.standard.string(forKey: "theme") ?? "light"221222// Keychain — secure storage (using wrapper)223let keychain = Keychain(service: "com.myapp")224try keychain.set(token, key: "auth_token")225let token = try keychain.get("auth_token")226227// Core Data — Swift228let fetchRequest: NSFetchRequest<Product> = Product.fetchRequest()229fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Product.name, ascending: true)]230let controller = NSFetchedResultsController(231 fetchRequest: fetchRequest,232 managedObjectContext: context,233 sectionNameKeyPath: nil,234 cacheName: nil235)236```237238```objc239// NSUserDefaults — Objective-C240[[NSUserDefaults standardUserDefaults] setObject:@"dark" forKey:@"theme"];241NSString *theme = [[NSUserDefaults standardUserDefaults] stringForKey:@"theme"] ?: @"light";242243// Core Data — Objective-C244NSFetchRequest *request = [Product fetchRequest];245request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]];246NSArray<Product *> *products = [context executeFetchRequest:request error:nil];247248// NSKeyedArchiver — legacy object persistence249NSData *data = [NSKeyedArchiver archivedDataWithRootObject:products250 requiringSecureCoding:YES251 error:nil];252[data writeToFile:path atomically:YES];253254// Keychain — Objective-C (Security framework)255NSDictionary *query = @{256 (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,257 (__bridge id)kSecAttrAccount: @"auth_token",258 (__bridge id)kSecValueData: [token dataUsingEncoding:NSUTF8StringEncoding]259};260SecItemAdd((__bridge CFDictionaryRef)query, NULL);261```262263### Step 4: Design the Data Layer264265**Repository pattern (recommended for all platforms):**266267```268UI ← ViewModel/BLoC ← Repository ← LocalDataSource (Room/CoreData/Drift)269 ← RemoteDataSource (API)270```271272| Strategy | Description | When to Use |273|----------|-------------|-------------|274| **Cache-aside** | App checks cache, fetches from network on miss | Simple caching, read-heavy |275| **Write-through** | Write to cache and network simultaneously | Data consistency critical |276| **Write-behind** | Write to cache immediately, sync to network async | Offline-first, write-heavy |277| **Refresh-ahead** | Proactively refresh cache before expiry | Predictable access patterns |278279### Step 5: Handle Migrations280281| Platform | Migration Tool | Strategy |282|----------|---------------|----------|283| **Flutter (Drift)** | Schema versioning + migration steps | Forward-only SQL migrations |284| **Flutter (Hive/Isar)** | Type adapter versioning | Schema-less, handle missing fields |285| **Android (Room)** | `Migration(fromVersion, toVersion)` | SQL ALTER statements |286| **iOS (Core Data)** | Lightweight migration (automatic) or mapping models | Prefer lightweight when possible |287| **iOS (SwiftData)** | `VersionedSchema` + `SchemaMigrationPlan` | Declarative migration stages |288289**Migration rules:**290- Never delete columns in production — deprecate and ignore291- Always test migrations with real production-like data292- Support skipping versions (1 → 3, not just 1 → 2 → 3)293- Back up database before destructive migrations294295### Step 6: Secure Sensitive Data296297| Data Type | Storage | Platform Implementation |298|-----------|---------|------------------------|299| **Auth tokens** | Secure storage | Keychain (iOS), EncryptedSharedPrefs (Android), flutter_secure_storage |300| **API keys** | Secure storage or build config | Never hardcode in source |301| **User PII** | Encrypted database | SQLCipher, encrypted Room, encrypted Core Data |302| **Biometric-gated data** | Hardware-backed secure storage | Keychain + biometric policy (iOS), Keystore + BiometricPrompt (Android) |303| **Session data** | Memory only | Never persist session tokens to disk unencrypted |304305## Output Format306307```markdown308## Persistence Summary309- **Platform:** [Flutter / Android / iOS]310- **Data types:** [What data needs to be stored]311- **Solutions chosen:** [key-value, SQL, NoSQL, secure storage]312313## Storage Architecture314[Data flow diagram: UI → Repository → Local/Remote sources]315316## Schema Design317[Core entities, relationships, indexes]318319## Migration Strategy320[How schema changes are handled]321322## Security323[How sensitive data is protected]324325## Sync Strategy (if applicable)326[How local data syncs with server]327```328329## Quality Checklist330331- [ ] Storage type matches data characteristics (don't use key-value for relational data)332- [ ] Sensitive data is in secure storage, not plain SharedPreferences/UserDefaults333- [ ] Database migrations are tested with production-like data334- [ ] Repository pattern isolates data source details from business logic335- [ ] Reactive queries are used where UI needs live updates336- [ ] Cache invalidation strategy is defined (TTL, explicit, event-based)337- [ ] Data is cleaned up on logout (tokens, cached PII)338- [ ] Storage size is monitored (especially on low-storage devices)339340## Edge Cases341342- SharedPreferences/UserDefaults are NOT encrypted — never store tokens or PII in them343- Core Data + iCloud sync has many edge cases (conflict resolution, account switching) — test thoroughly344- Room migrations that fail will destroy the database by default — always provide migration paths or use `fallbackToDestructiveMigration()` only in dev345- On Android, `MODE_WORLD_READABLE` SharedPreferences is deprecated and insecure — always use `MODE_PRIVATE`346- iOS Keychain items persist across app reinstalls by default — set `kSecAttrAccessible` appropriately347- For large binary files (images, videos), store the file on disk and keep only the path/reference in the database348- SQLite has a practical limit of ~1GB per database on mobile — shard or archive old data for larger datasets