| name | android-data-persistence |
| description | Use when designing Android local storage, Room migrations, DataStore preferences, or offline API synchronisation; use android-development for broader application structure. |
| metadata | {"portable":true,"compatible_with":["claude-code","codex"]} |
Platform Notes
- Optional helper plugins may help in some environments, but they must not be treated as required for this skill.
Android Data Persistence
Acknowledgement: Shared by Peter Bamuhigire, techguypeter.com, +256 784 464178.
Use When
- Android data persistence standards with Room as primary local storage and custom API backends for cloud sync. Covers SharedPreferences, DataStore, Room (entities, DAOs, relations, migrations), file storage, offline-first architecture, and...
Evidence Produced
| Category | Artifact | Format | Example |
|---|
| Data safety | Persistence model spec | Markdown doc per skill-composition-standards/references/entity-model-template.md covering Room entities and DAOs | docs/android/persistence-model-orders.md |
| Correctness | Persistence test plan | Markdown doc listing CRUD, migration, and FTS test cases | docs/android/persistence-tests-orders.md |
References
- Use the
references/ directory for deep detail after reading the core workflow below.
references/android-room.md for Room entities, DAOs, relations, migrations, encryption, paging, FTS, and testing.
Overview
Our apps use Room for local persistence and custom REST API backends for cloud data. This skill covers every storage method and when to use each.
Android 10+ required.
Architecture: Offline-first with API sync via Repository pattern.
Offline-First is MANDATORY. Every app we build MUST work fully offline. Users in areas
with poor or intermittent network must never be blocked. Sync happens automatically when
connectivity returns — the user must never know or notice. See references/api-sync-patterns.md
for the complete sync engine with guaranteed no-duplicates, no-missing-transactions.
Room Deep Reference: For full Room API (FTS4, views, migrations, encryption, paging,
conflict resolution, testing), load references/android-room.md alongside this skill.
Backend Environments: APIs run on Windows dev (MySQL 8.4.7), Ubuntu staging (MySQL 8.x), Debian production (MySQL 8.x). Use Gradle build flavors for environment-specific base URLs. All backends use utf8mb4_unicode_ci collation.
Icon Policy: If any UI code is included, use custom PNG icons and maintain PROJECT_ICONS.md (see mobile-platform-operations).
Report Table Policy: If persistence examples include report UIs that can exceed 25 rows, use table layouts (see mobile-reports).
UI (Compose) → ViewModel → Repository → Room (local) + API (remote)
Storage Decision Guide
| Need | Solution | Complexity |
|---|
| App settings, flags, tokens | DataStore / SharedPreferences | Very Low |
| Structured data (offline) | Room | Medium |
| Large files (images, docs) | Internal/External files | Low |
| Cloud-synced data | Room + API backend | Medium-High |
| Real-time shared data | API with polling/WebSocket | High |
| Cached API responses | Room as cache layer | Medium |
Quick Decision
"I need to store..."
├── Settings/tokens/flags → DataStore (Preferences)
├── A single file → Internal storage
├── Structured local data → Room
├── Data from our API → Room cache + Repository sync
└── User-generated media → Internal files + API upload
Quick Reference
| Topic | Reference File | When to Use |
|---|
| Room Essentials | references/room-essentials.md | Entities, DAOs, Database setup, TypeConverters |
| Room Advanced | references/room-advanced.md | Relations, migrations, testing, performance |
| Local Storage | references/local-storage.md | DataStore, SharedPreferences, file I/O |
| API Sync Patterns | references/api-sync-patterns.md | Idempotent sync, no duplicates, no missing transactions, WorkManager |
| Room Deep Reference | references/android-room.md | FTS4, views, paging, SQLCipher, migrations, conflict resolution |
Room: The Primary Local Database
Room is our standard for all structured local data. It provides compile-time SQL verification, lifecycle-aware queries, and clean integration with ViewModels.
Three Core Components
@Entity → Defines a database table (data class)
@Dao → Defines operations (interface)
@Database → Connects entities and DAOs (abstract class)
Entity (Table Definition)
@Entity(tableName = "products")
data class ProductEntity(
@PrimaryKey
@ColumnInfo(name = "product_id")
val productId: String,
@ColumnInfo(name = "name")
val name: String,
@ColumnInfo(name = "price")
val price: Double,
@ColumnInfo(name = "category_id")
val categoryId: String,
@ColumnInfo(name = "last_synced")
val lastSynced: Long = System.currentTimeMillis()
)
DAO (Data Access)
@Dao
interface ProductDao {
@Query("SELECT * FROM products ORDER BY name ASC")
fun getAllProducts(): Flow<List<ProductEntity>>
@Query("SELECT * FROM products WHERE product_id = :id")
suspend fun getById(id: String): ProductEntity?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(products: List<ProductEntity>)
@Update
suspend fun update(product: ProductEntity)
@Delete
suspend fun delete(product: ProductEntity)
@Query("DELETE FROM products")
suspend fun deleteAll()
}
Database
@Database(
entities = [ProductEntity::class, CategoryEntity::class],
version = 1,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun productDao(): ProductDao
abstract fun categoryDao(): CategoryDao
}
Hilt Module for Database
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app_database"
)
.addMigrations(MIGRATION_1_2)
.build()
}
@Provides
fun provideProductDao(database: AppDatabase): ProductDao = database.productDao()
}
Repository Pattern (Room + API)
The Repository is the single source of truth for data:
class ProductRepository @Inject constructor(
private val productDao: ProductDao,
private val apiService: ProductApiService
) {
fun getProducts(): Flow<List<Product>> =
productDao.getAllProducts().map { entities ->
entities.map { it.toDomain() }
}
suspend fun refreshProducts(): Result<Unit> {
return try {
val response = apiService.getProducts()
productDao.insertAll(response.map { it.toEntity() })
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun createProduct(product: Product): Result<Product> {
return try {
val response = apiService.createProduct(product.toDto())
val entity = response.toEntity()
productDao.insertAll(listOf(entity))
Result.success(entity.toDomain())
} catch (e: Exception) {
Result.failure(e)
}
}
}
ViewModel Integration
@HiltViewModel
class ProductViewModel @Inject constructor(
private val repository: ProductRepository
) : ViewModel() {
val products = repository.getProducts()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
private val _isRefreshing = MutableStateFlow(false)
val isRefreshing = _isRefreshing.asStateFlow()
fun refresh() {
viewModelScope.launch {
_isRefreshing.value = true
repository.refreshProducts()
_isRefreshing.value = false
}
}
}
DataStore (Settings & Preferences)
Prefer DataStore over SharedPreferences for new code:
object PrefsKeys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val AUTH_TOKEN = stringPreferencesKey("auth_token")
val SORT_ORDER = stringPreferencesKey("sort_order")
}
val darkMode: Flow<Boolean> = context.dataStore.data.map { prefs ->
prefs[PrefsKeys.DARK_MODE] ?: false
}
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[PrefsKeys.DARK_MODE] = enabled
}
}
TypeConverters (Custom Column Types)
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
@TypeConverter fun fromStringList(value: String?): List<String> =
if (value.isNullOrBlank()) emptyList() else value.split("\u001F")
@TypeConverter fun toStringList(list: List<String>): String = list.joinToString("\u001F")
}
Migrations
Always provide migration paths when changing schema:
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE products ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1")
}
}
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
Never use fallbackToDestructiveMigration() in production.
Data Layer Mapping
Always separate API DTOs, Room entities, and domain models:
data class ProductDto(val id: String, val name: String, val price: Double)
@Entity(tableName = "products")
data class ProductEntity(
@PrimaryKey val productId: String,
val name: String,
val price: Double,
val lastSynced: Long
)
data class Product(val id: String, val name: String, val price: Double)
fun ProductDto.toEntity() = ProductEntity(id, name, price, System.currentTimeMillis())
fun ProductEntity.toDomain() = Product(productId, name, price)
fun Product.toDto() = ProductDto(id, name, price)
Patterns & Anti-Patterns
DO
- Use Room for all structured local data
- Use DataStore for key-value preferences (not SharedPreferences)
- Use Repository pattern as single source of truth
- Separate DTOs, entities, and domain models
- Return
Flow from DAOs for reactive UI updates
- Provide proper migrations for schema changes
- Use
onConflict = REPLACE for API-synced data
- Export Room schema for migration testing
DON'T
- Access DAOs directly from ViewModels (use Repository)
- Store large blobs in Room (use file storage)
- Use
fallbackToDestructiveMigration() in production
- Mix network calls with database operations outside Repository
- Store sensitive data unencrypted (use EncryptedSharedPreferences)
- Skip the entity-to-domain mapping (couples UI to database schema)
- Run Room queries on the main thread
Integration with Other Skills
references/android-room.md → Deep Room API (entities, FTS4, views, migrations, SQLCipher, paging)
↓
android-data-persistence → Offline sync engine (THIS SKILL)
↓
android-development → Clean Architecture, Hilt DI, MVVM
↓
android-tdd → DAO tests, migration tests, SyncWorker tests
Key integrations:
- references/android-room.md: All Room patterns; always load with this skill for full coverage
- android-tdd: Test DAOs with in-memory DB, SyncWorker with TestWorkerFactory
- api-error-handling: Error patterns for sync failures and HTTP 409 conflicts
References
- Room Guide: developer.android.com/training/data-storage/room
- DataStore: developer.android.com/topic/libraries/architecture/datastore
- Architecture Samples: github.com/android/architecture-samples
Decision Rules
| Data or failure mode | Required choice |
|---|
| Structured records queried offline | Room with exported schemas and explicit migrations |
| Small non-secret preferences | DataStore |
| Credentials or cryptographic material | Keystore-backed encrypted storage, never a Room plaintext column |
| Financial or stock conflict | Server-authoritative reconciliation with an idempotency key |
Capability Contract And Degraded Mode
Read and search the data layer before recommending schema changes. Edit and execute migration tests only when authorised. Without execution, return the proposed entity, migration SQL, rollback risk, and unverified test commands.
Domain Anti-Patterns
- Using destructive migration in production. Fix: write and test every version hop.
- Treating network data as the UI source of truth. Fix: observe Room and synchronise behind the repository.
- Storing secrets in DataStore or Room. Fix: use Keystore-backed encryption.
- Retrying writes without idempotency keys. Fix: persist an outbox identifier through acknowledgement.
- Resolving monetary conflicts with client timestamps. Fix: make the ledger service authoritative.
Inputs
| Artefact | Required? | Purpose |
|---|
| Data model, access patterns, offline/sync, migration, and retention requirements | yes | Select persistence design |
Outputs
- Produce Android persistence architecture, schemas, migrations, tests, and recovery evidence.
Degraded mode
Fallback without a device/database test environment: produce schemas and migration tests as unverified plans; do not claim upgrade safety.
Capability contract
Database writes, destructive migrations, and device-state resets require explicit test scope and recoverable fixtures.