android-data-persistence/SKILL.md
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...
npx skillsauth add peterbamuhigire/skills-web-dev android-data-persistenceInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
android-data-persistence or would be better handled by a more specific companion skill.references only as needed.SKILL.md first, then load only the referenced deep-dive files that are necessary for the task.references/ directory for deep detail after reading the core workflow below.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), use the android-room skill alongside this one.
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 android-custom-icons).
Report Table Policy: If persistence examples include report UIs that can exceed 25 rows, use table layouts (see android-report-tables).
UI (Compose) → ViewModel → Repository → Room (local) + API (remote)
| 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 |
"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
| 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 | android-room skill | FTS4, views, paging, SQLCipher, migrations, conflict resolution |
Room is our standard for all structured local data. It provides compile-time SQL verification, lifecycle-aware queries, and clean integration with ViewModels.
@Entity → Defines a database table (data class)
@Dao → Defines operations (interface)
@Database → Connects entities and DAOs (abstract class)
@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
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(
entities = [ProductEntity::class, CategoryEntity::class],
version = 1,
exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun productDao(): ProductDao
abstract fun categoryDao(): CategoryDao
}
@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()
}
The Repository is the single source of truth for data:
class ProductRepository @Inject constructor(
private val productDao: ProductDao,
private val apiService: ProductApiService
) {
// Local data as Flow (always fresh from Room)
fun getProducts(): Flow<List<Product>> =
productDao.getAllProducts().map { entities ->
entities.map { it.toDomain() }
}
// SIMPLIFIED EXAMPLE — for production offline-first repository with PendingActionDao,
// SyncCursorDao, and no-duplicate/no-missing-transaction guarantees, see:
// references/api-sync-patterns.md Section 3 (ProductRepository)
// Sync: fetch from API, save to Room
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)
}
}
// Create: save locally + push to API
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)
}
}
}
@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
}
}
}
Prefer DataStore over SharedPreferences for new code:
// Define keys
object PrefsKeys {
val DARK_MODE = booleanPreferencesKey("dark_mode")
val AUTH_TOKEN = stringPreferencesKey("auth_token")
val SORT_ORDER = stringPreferencesKey("sort_order")
}
// Read
val darkMode: Flow<Boolean> = context.dataStore.data.map { prefs ->
prefs[PrefsKeys.DARK_MODE] ?: false
}
// Write
suspend fun setDarkMode(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[PrefsKeys.DARK_MODE] = enabled
}
}
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")
// Using \u001F (unit separator) — commas in values corrupt comma-delimited lists
}
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")
}
}
// Register in database builder
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3)
.build()
Never use fallbackToDestructiveMigration() in production.
Always separate API DTOs, Room entities, and domain models:
// API DTO (what the server sends)
data class ProductDto(val id: String, val name: String, val price: Double)
// Room Entity (what's stored locally)
@Entity(tableName = "products")
data class ProductEntity(
@PrimaryKey val productId: String,
val name: String,
val price: Double,
val lastSynced: Long
)
// Domain Model (what the UI uses)
data class Product(val id: String, val name: String, val price: Double)
// Mappers
fun ProductDto.toEntity() = ProductEntity(id, name, price, System.currentTimeMillis())
fun ProductEntity.toDomain() = Product(productId, name, price)
fun Product.toDto() = ProductDto(id, name, price)
Flow from DAOs for reactive UI updatesonConflict = REPLACE for API-synced datafallbackToDestructiveMigration() in productionandroid-room → 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:
data-ai
Use when adding AI-powered analytics to a SaaS platform — semantic search over business data, natural language queries, trend detection, anomaly alerts, and AI-generated insights for dashboards. Covers embeddings, NL2SQL, and per-tenant analytics...
data-ai
Design AI-powered analytics dashboards — what metrics to show, how to display AI predictions and confidence, drill-down patterns, KPI cards, trend visualisation, AI Insights panels, export design, and role-based dashboard variants. Invoke when...
development
Use when designing, building, reviewing, or upgrading production software systems that must be secure, performant, maintainable, scalable, and user-centered. Apply before writing specs, code, architecture, APIs, databases, mobile apps, SaaS platforms, or ERP systems.
development
Professional web app UI using commercial templates (Tabler/Bootstrap 5) with strong frontend design direction when needed. Use for CRUD interfaces, dashboards, admin panels with SweetAlert2, DataTables, Flatpickr. Clone seeder-page.php, use...