android-sdd 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: offline-first
|
|
3
|
+
description: >
|
|
4
|
+
Offline-first architecture for Android apps.
|
|
5
|
+
Load this skill when designing apps that must work without network,
|
|
6
|
+
implementing local-first data access, syncing data with a remote server,
|
|
7
|
+
handling network availability changes, or designing cache invalidation strategy.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Offline First
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Offline-first means the app reads and writes to local storage first. The network is treated as an unreliable sync mechanism — not a requirement. The local database is the single source of truth; the UI observes local data, and a sync engine reconciles local state with the remote server in the background.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- **Local DB is the source of truth** — UI never reads directly from network
|
|
21
|
+
- **Write locally first** — operations succeed immediately, sync later
|
|
22
|
+
- **Observe local, sync remotely** — UI observes Room/DataStore, background sync updates local DB
|
|
23
|
+
- **Surface sync status** — UI shows sync state, errors, last-synced time
|
|
24
|
+
- **Idempotent sync** — syncing the same data twice must not corrupt state
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Architecture Pattern
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
UI → ViewModel → UseCase → Repository
|
|
32
|
+
│
|
|
33
|
+
┌─────────┴─────────┐
|
|
34
|
+
▼ ▼
|
|
35
|
+
LocalDataSource RemoteDataSource
|
|
36
|
+
(Room / DataStore) (Retrofit / Ktor)
|
|
37
|
+
│
|
|
38
|
+
▼
|
|
39
|
+
Single Source of Truth
|
|
40
|
+
(UI observes this)
|
|
41
|
+
|
|
42
|
+
SyncManager (WorkManager) periodically:
|
|
43
|
+
1. Fetches remote data
|
|
44
|
+
2. Writes to LocalDataSource
|
|
45
|
+
3. UI updates automatically via Flow
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Repository Implementation
|
|
51
|
+
|
|
52
|
+
```kotlin
|
|
53
|
+
// ✅ Repository — reads local, triggers sync, exposes Flow
|
|
54
|
+
class UserRepositoryImpl @Inject constructor(
|
|
55
|
+
private val localDataSource: UserLocalDataSource,
|
|
56
|
+
private val remoteDataSource: UserRemoteDataSource,
|
|
57
|
+
private val syncManager: SyncManager
|
|
58
|
+
) : UserRepository {
|
|
59
|
+
|
|
60
|
+
// ✅ Always return local Flow — UI updates automatically when sync writes to DB
|
|
61
|
+
override fun observeUsers(): Flow<List<User>> =
|
|
62
|
+
localDataSource.observeAll().map { entities -> entities.map { it.toDomain() } }
|
|
63
|
+
|
|
64
|
+
// ✅ Read-through cache
|
|
65
|
+
override suspend fun getUser(id: String): Result<User> = runCatching {
|
|
66
|
+
localDataSource.getById(id)?.toDomain()
|
|
67
|
+
?: fetchAndCacheUser(id)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ✅ Write-through: write locally first, enqueue remote sync
|
|
71
|
+
override suspend fun updateUser(user: User): Result<Unit> = runCatching {
|
|
72
|
+
localDataSource.upsert(user.toEntity().copy(syncStatus = SyncStatus.PENDING))
|
|
73
|
+
syncManager.enqueueUserSync()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private suspend fun fetchAndCacheUser(id: String): User {
|
|
77
|
+
val dto = remoteDataSource.getUser(id)
|
|
78
|
+
val entity = dto.toDomain().toEntity().copy(syncStatus = SyncStatus.SYNCED)
|
|
79
|
+
localDataSource.upsert(entity)
|
|
80
|
+
return entity.toDomain()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Sync Status Tracking
|
|
88
|
+
|
|
89
|
+
```kotlin
|
|
90
|
+
// ✅ Track sync state per entity
|
|
91
|
+
enum class SyncStatus { SYNCED, PENDING, FAILED }
|
|
92
|
+
|
|
93
|
+
@Entity(tableName = "users")
|
|
94
|
+
data class UserEntity(
|
|
95
|
+
@PrimaryKey val id: String,
|
|
96
|
+
val name: String,
|
|
97
|
+
val email: String,
|
|
98
|
+
val syncStatus: SyncStatus = SyncStatus.SYNCED,
|
|
99
|
+
val lastModified: Long = System.currentTimeMillis()
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// ✅ DAO queries for pending sync
|
|
103
|
+
@Dao
|
|
104
|
+
interface UserDao {
|
|
105
|
+
@Query("SELECT * FROM users WHERE syncStatus = 'PENDING'")
|
|
106
|
+
suspend fun getPendingSync(): List<UserEntity>
|
|
107
|
+
|
|
108
|
+
@Query("UPDATE users SET syncStatus = :status WHERE id = :id")
|
|
109
|
+
suspend fun updateSyncStatus(id: String, status: SyncStatus)
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Sync Engine with WorkManager
|
|
116
|
+
|
|
117
|
+
```kotlin
|
|
118
|
+
// ✅ Periodic sync via WorkManager
|
|
119
|
+
class UserSyncWorker @AssistedInject constructor(
|
|
120
|
+
@Assisted context: Context,
|
|
121
|
+
@Assisted workerParams: WorkerParameters,
|
|
122
|
+
private val userRepository: UserRepository,
|
|
123
|
+
private val remoteDataSource: UserRemoteDataSource,
|
|
124
|
+
private val localDataSource: UserLocalDataSource
|
|
125
|
+
) : CoroutineWorker(context, workerParams) {
|
|
126
|
+
|
|
127
|
+
override suspend fun doWork(): Result {
|
|
128
|
+
return try {
|
|
129
|
+
// Push pending changes
|
|
130
|
+
val pending = localDataSource.getPendingSync()
|
|
131
|
+
pending.forEach { entity ->
|
|
132
|
+
remoteDataSource.updateUser(entity.toDto())
|
|
133
|
+
localDataSource.updateSyncStatus(entity.id, SyncStatus.SYNCED)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Pull remote changes
|
|
137
|
+
val remoteUsers = remoteDataSource.getUsers()
|
|
138
|
+
localDataSource.upsertAll(remoteUsers.map { it.toDomain().toEntity() })
|
|
139
|
+
|
|
140
|
+
Result.success()
|
|
141
|
+
} catch (e: Exception) {
|
|
142
|
+
if (runAttemptCount < 3) Result.retry() else Result.failure()
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
companion object {
|
|
147
|
+
fun schedule(workManager: WorkManager) {
|
|
148
|
+
val request = PeriodicWorkRequestBuilder<UserSyncWorker>(15, TimeUnit.MINUTES)
|
|
149
|
+
.setConstraints(
|
|
150
|
+
Constraints.Builder()
|
|
151
|
+
.setRequiredNetworkType(NetworkType.CONNECTED)
|
|
152
|
+
.build()
|
|
153
|
+
)
|
|
154
|
+
.build()
|
|
155
|
+
workManager.enqueueUniquePeriodicWork(
|
|
156
|
+
"user_sync",
|
|
157
|
+
ExistingPeriodicWorkPolicy.KEEP,
|
|
158
|
+
request
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Network Connectivity Observation
|
|
168
|
+
|
|
169
|
+
```kotlin
|
|
170
|
+
// ✅ Observe network state
|
|
171
|
+
class NetworkMonitor @Inject constructor(
|
|
172
|
+
@ApplicationContext private val context: Context
|
|
173
|
+
) {
|
|
174
|
+
val isOnline: Flow<Boolean> = callbackFlow {
|
|
175
|
+
val manager = context.getSystemService(ConnectivityManager::class.java)
|
|
176
|
+
|
|
177
|
+
val callback = object : ConnectivityManager.NetworkCallback() {
|
|
178
|
+
override fun onAvailable(network: Network) { trySend(true) }
|
|
179
|
+
override fun onLost(network: Network) { trySend(false) }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
val request = NetworkRequest.Builder()
|
|
183
|
+
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
184
|
+
.build()
|
|
185
|
+
|
|
186
|
+
manager.registerNetworkCallback(request, callback)
|
|
187
|
+
trySend(manager.activeNetwork != null)
|
|
188
|
+
|
|
189
|
+
awaitClose { manager.unregisterNetworkCallback(callback) }
|
|
190
|
+
}.distinctUntilChanged()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ✅ Surface network state in ViewModel
|
|
194
|
+
@HiltViewModel
|
|
195
|
+
class HomeViewModel @Inject constructor(
|
|
196
|
+
private val networkMonitor: NetworkMonitor
|
|
197
|
+
) : ViewModel() {
|
|
198
|
+
|
|
199
|
+
val isOffline: StateFlow<Boolean> = networkMonitor.isOnline
|
|
200
|
+
.map { !it }
|
|
201
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), false)
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## UI: Offline Banner
|
|
208
|
+
|
|
209
|
+
```kotlin
|
|
210
|
+
// ✅ Show offline indicator when not connected
|
|
211
|
+
@Composable
|
|
212
|
+
fun OfflineBanner(isOffline: Boolean) {
|
|
213
|
+
AnimatedVisibility(visible = isOffline) {
|
|
214
|
+
Surface(color = MaterialTheme.colorScheme.errorContainer) {
|
|
215
|
+
Row(
|
|
216
|
+
modifier = Modifier
|
|
217
|
+
.fillMaxWidth()
|
|
218
|
+
.padding(horizontal = 16.dp, vertical = 8.dp),
|
|
219
|
+
horizontalArrangement = Arrangement.Center
|
|
220
|
+
) {
|
|
221
|
+
Icon(Icons.Default.WifiOff, contentDescription = null)
|
|
222
|
+
Spacer(Modifier.width(8.dp))
|
|
223
|
+
Text("You're offline", style = MaterialTheme.typography.labelMedium)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Anti-Patterns
|
|
233
|
+
|
|
234
|
+
- Reading directly from network in ViewModel — always read from local source
|
|
235
|
+
- No sync status tracking — can't tell which records need to be pushed
|
|
236
|
+
- Replacing entire local DB on every sync — causes UI flicker; use upsert
|
|
237
|
+
- Syncing on main thread — always use WorkManager or background coroutine
|
|
238
|
+
- No conflict resolution strategy — when local and remote diverge, there must be a rule
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Related Skills
|
|
243
|
+
|
|
244
|
+
- `room` — local database for offline storage
|
|
245
|
+
- `sync-engine` — advanced sync patterns and conflict resolution
|
|
246
|
+
- `workmanager` — background sync scheduling
|
|
247
|
+
- `conflict-resolution` — handling remote/local data conflicts
|
|
248
|
+
- `repository-pattern` — repository wiring for offline-first
|
|
249
|
+
- `cache-strategy` — cache invalidation and expiry
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: repository-pattern
|
|
3
|
+
description: >
|
|
4
|
+
Repository pattern for Android data access abstraction.
|
|
5
|
+
Load this skill when implementing a Repository, defining its interface,
|
|
6
|
+
coordinating between local and remote data sources, handling caching logic,
|
|
7
|
+
or structuring the data layer of a feature.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Repository Pattern
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
The Repository pattern abstracts data sources behind a single interface. The ViewModel and UseCases never know whether data comes from Room, Retrofit, DataStore, or memory cache — they only interact with the Repository interface defined in the Domain layer. The implementation in the Data layer coordinates between local and remote sources.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Repository **interface** lives in Domain — pure Kotlin, no Android dependencies
|
|
21
|
+
- Repository **implementation** lives in Data — knows about Room, Retrofit, etc.
|
|
22
|
+
- Repository is the **only entry point** to data — no direct DAO or API calls from UseCases
|
|
23
|
+
- Repositories return **Domain models** — never DTOs or Entities
|
|
24
|
+
- One repository per **aggregate root** — not per screen or per DAO
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Interface (Domain Layer)
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ Interface in domain — clean, no framework dependencies
|
|
32
|
+
interface UserRepository {
|
|
33
|
+
// Reactive — for UI observation
|
|
34
|
+
fun observeUsers(): Flow<List<User>>
|
|
35
|
+
fun observeUser(id: String): Flow<User?>
|
|
36
|
+
|
|
37
|
+
// Suspend — for one-time reads and writes
|
|
38
|
+
suspend fun getUser(id: String): Result<User>
|
|
39
|
+
suspend fun getUsers(): Result<List<User>>
|
|
40
|
+
suspend fun createUser(user: User): Result<User>
|
|
41
|
+
suspend fun updateUser(user: User): Result<Unit>
|
|
42
|
+
suspend fun deleteUser(id: String): Result<Unit>
|
|
43
|
+
|
|
44
|
+
// Domain operations
|
|
45
|
+
suspend fun searchUsers(query: String): Result<List<User>>
|
|
46
|
+
suspend fun syncUsers(): Result<Unit>
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Implementation (Data Layer)
|
|
53
|
+
|
|
54
|
+
```kotlin
|
|
55
|
+
// ✅ Implementation in data layer — coordinates sources
|
|
56
|
+
class UserRepositoryImpl @Inject constructor(
|
|
57
|
+
private val userDao: UserDao,
|
|
58
|
+
private val userApiService: UserApiService,
|
|
59
|
+
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
|
|
60
|
+
) : UserRepository {
|
|
61
|
+
|
|
62
|
+
// ✅ Observe local DB — UI updates automatically when DB changes
|
|
63
|
+
override fun observeUsers(): Flow<List<User>> =
|
|
64
|
+
userDao.observeAll()
|
|
65
|
+
.map { entities -> entities.map { it.toDomain() } }
|
|
66
|
+
.flowOn(dispatcher)
|
|
67
|
+
|
|
68
|
+
// ✅ Cache-first read
|
|
69
|
+
override suspend fun getUser(id: String): Result<User> = withContext(dispatcher) {
|
|
70
|
+
runCatching {
|
|
71
|
+
userDao.getById(id)?.toDomain()
|
|
72
|
+
?: fetchAndCacheUser(id)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ✅ Write to local + remote
|
|
77
|
+
override suspend fun createUser(user: User): Result<User> = withContext(dispatcher) {
|
|
78
|
+
runCatching {
|
|
79
|
+
val dto = userApiService.createUser(user.toCreateRequest())
|
|
80
|
+
val created = dto.toDomain()
|
|
81
|
+
userDao.insert(created.toEntity())
|
|
82
|
+
created
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override suspend fun updateUser(user: User): Result<Unit> = withContext(dispatcher) {
|
|
87
|
+
runCatching {
|
|
88
|
+
userApiService.updateUser(user.id, user.toUpdateRequest())
|
|
89
|
+
userDao.upsert(user.toEntity())
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
override suspend fun deleteUser(id: String): Result<Unit> = withContext(dispatcher) {
|
|
94
|
+
runCatching {
|
|
95
|
+
userApiService.deleteUser(id)
|
|
96
|
+
userDao.deleteById(id)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override suspend fun syncUsers(): Result<Unit> = withContext(dispatcher) {
|
|
101
|
+
runCatching {
|
|
102
|
+
val dtos = userApiService.getUsers()
|
|
103
|
+
val entities = dtos.map { it.toDomain().toEntity() }
|
|
104
|
+
userDao.replaceAll(entities)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private suspend fun fetchAndCacheUser(id: String): User {
|
|
109
|
+
val dto = userApiService.getUser(id)
|
|
110
|
+
val domain = dto.toDomain()
|
|
111
|
+
userDao.insert(domain.toEntity())
|
|
112
|
+
return domain
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Hilt Binding
|
|
120
|
+
|
|
121
|
+
```kotlin
|
|
122
|
+
// ✅ Bind interface to implementation
|
|
123
|
+
@Module
|
|
124
|
+
@InstallIn(SingletonComponent::class)
|
|
125
|
+
abstract class UserRepositoryModule {
|
|
126
|
+
|
|
127
|
+
@Binds
|
|
128
|
+
@Singleton
|
|
129
|
+
abstract fun bindUserRepository(
|
|
130
|
+
impl: UserRepositoryImpl
|
|
131
|
+
): UserRepository
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Repository Strategies
|
|
138
|
+
|
|
139
|
+
```kotlin
|
|
140
|
+
// ✅ Strategy 1: Network-first (always fresh data)
|
|
141
|
+
override suspend fun getUser(id: String): Result<User> = runCatching {
|
|
142
|
+
val dto = userApiService.getUser(id)
|
|
143
|
+
val domain = dto.toDomain()
|
|
144
|
+
userDao.upsert(domain.toEntity())
|
|
145
|
+
domain
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ✅ Strategy 2: Cache-first (fast, offline-capable)
|
|
149
|
+
override suspend fun getUser(id: String): Result<User> = runCatching {
|
|
150
|
+
userDao.getById(id)?.toDomain() ?: run {
|
|
151
|
+
val dto = userApiService.getUser(id)
|
|
152
|
+
val domain = dto.toDomain()
|
|
153
|
+
userDao.insert(domain.toEntity())
|
|
154
|
+
domain
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ✅ Strategy 3: Cache-then-network (show cached, refresh in background)
|
|
159
|
+
override fun observeUserWithRefresh(id: String): Flow<User> = flow {
|
|
160
|
+
userDao.getById(id)?.toDomain()?.let { emit(it) }
|
|
161
|
+
val refreshed = userApiService.getUser(id).toDomain()
|
|
162
|
+
userDao.upsert(refreshed.toEntity())
|
|
163
|
+
emit(refreshed)
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Fake Repository for Testing
|
|
170
|
+
|
|
171
|
+
```kotlin
|
|
172
|
+
// ✅ Fake implementation for unit tests — no Room, no Retrofit
|
|
173
|
+
class FakeUserRepository : UserRepository {
|
|
174
|
+
private val users = MutableStateFlow<List<User>>(emptyList())
|
|
175
|
+
var shouldThrow = false
|
|
176
|
+
|
|
177
|
+
override fun observeUsers(): Flow<List<User>> = users
|
|
178
|
+
|
|
179
|
+
override suspend fun getUser(id: String): Result<User> {
|
|
180
|
+
if (shouldThrow) return Result.failure(Exception("Network error"))
|
|
181
|
+
return users.value.find { it.id == id }
|
|
182
|
+
?.let { Result.success(it) }
|
|
183
|
+
?: Result.failure(Exception("Not found"))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
override suspend fun createUser(user: User): Result<User> {
|
|
187
|
+
users.update { it + user }
|
|
188
|
+
return Result.success(user)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fun emit(userList: List<User>) {
|
|
192
|
+
users.value = userList
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Anti-Patterns
|
|
200
|
+
|
|
201
|
+
- DAO called directly from ViewModel or UseCase — always go through Repository
|
|
202
|
+
- Repository returning Entity or DTO — always map to Domain model before returning
|
|
203
|
+
- One Repository per screen — one Repository per aggregate root (User, Order, Product)
|
|
204
|
+
- Repository containing UI logic — Repository only handles data access and mapping
|
|
205
|
+
- Not using `flowOn(dispatcher)` — DB and network operations must not run on Main thread
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Related Skills
|
|
210
|
+
|
|
211
|
+
- `clean-architecture` — where Repository fits in layer structure
|
|
212
|
+
- `dao` — DAO patterns the Repository coordinates
|
|
213
|
+
- `dto-mapping` — mapping between DTO/Entity and Domain model
|
|
214
|
+
- `use-case-design` — UseCases that call Repositories
|
|
215
|
+
- `cache-strategy` — cache invalidation and expiry policies
|
|
216
|
+
- `offline-first` — Repository design for offline-capable apps
|