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,261 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cache-strategy
|
|
3
|
+
description: >
|
|
4
|
+
Caching patterns and strategies for Android apps — in-memory cache,
|
|
5
|
+
disk cache, network cache, and cache invalidation.
|
|
6
|
+
Load this skill when designing data caching, deciding cache lifetime,
|
|
7
|
+
implementing offline-first patterns, or managing stale data.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Cache Strategy
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Caching stores copies of data closer to where it's needed to reduce latency, network usage, and battery drain. A good cache strategy balances freshness (up-to-date data) with performance (fast reads). The right strategy depends on how often data changes and how critical freshness is.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Cache data at the **repository layer** — not in ViewModel or UI
|
|
21
|
+
- Define explicit **TTL (time-to-live)** for every cache — stale data is a bug
|
|
22
|
+
- **Room is the cache** in offline-first architecture — network fills it, UI reads from it
|
|
23
|
+
- Never cache **sensitive data** unencrypted
|
|
24
|
+
- Provide a way to **force-refresh** — users expect a pull-to-refresh to work
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Cache Strategies
|
|
29
|
+
|
|
30
|
+
| Strategy | When to Use |
|
|
31
|
+
| -------------------------- | ---------------------------------------------------------- |
|
|
32
|
+
| **Cache-first** | Offline-first, data changes infrequently |
|
|
33
|
+
| **Network-first** | Data must be fresh, connectivity is reliable |
|
|
34
|
+
| **Cache-then-network** | Show cached data immediately, update when network responds |
|
|
35
|
+
| **TTL-based** | Cache valid for N minutes, refresh after expiry |
|
|
36
|
+
| **Stale-while-revalidate** | Show stale data, fetch fresh in background |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Offline-First with Room as Cache
|
|
41
|
+
|
|
42
|
+
```kotlin
|
|
43
|
+
// ✅ Room is the single source of truth — network populates it
|
|
44
|
+
class UserRepository @Inject constructor(
|
|
45
|
+
private val userDao: UserDao,
|
|
46
|
+
private val userApi: UserApi,
|
|
47
|
+
private val userMapper: UserMapper
|
|
48
|
+
) {
|
|
49
|
+
|
|
50
|
+
// UI always reads from Room — reactive, always fresh
|
|
51
|
+
fun observeUsers(): Flow<List<User>> =
|
|
52
|
+
userDao.observeAll().map { it.map(userMapper::toDomain) }
|
|
53
|
+
|
|
54
|
+
// Refresh from network → saves to Room → Room emits new value
|
|
55
|
+
suspend fun refreshUsers(): Result<Unit> = runCatching {
|
|
56
|
+
val remote = userApi.getUsers()
|
|
57
|
+
userDao.upsertAll(remote.map(userMapper::toEntity))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## TTL-Based Cache
|
|
65
|
+
|
|
66
|
+
```kotlin
|
|
67
|
+
// ✅ Track last fetch time — refresh only when stale
|
|
68
|
+
class UserRepository @Inject constructor(
|
|
69
|
+
private val userDao: UserDao,
|
|
70
|
+
private val userApi: UserApi,
|
|
71
|
+
private val prefs: UserPreferencesRepository,
|
|
72
|
+
private val clock: Clock = Clock.systemUTC()
|
|
73
|
+
) {
|
|
74
|
+
companion object {
|
|
75
|
+
val CACHE_TTL = Duration.ofMinutes(15)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fun observeUsers(): Flow<List<User>> = userDao.observeAll()
|
|
79
|
+
.map { it.map(userMapper::toDomain) }
|
|
80
|
+
|
|
81
|
+
suspend fun refreshIfStale() {
|
|
82
|
+
val lastSync = prefs.getLastSyncTime()
|
|
83
|
+
val isStale = lastSync == null ||
|
|
84
|
+
Duration.between(lastSync, clock.instant()) > CACHE_TTL
|
|
85
|
+
|
|
86
|
+
if (isStale) {
|
|
87
|
+
refreshUsers()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private suspend fun refreshUsers() {
|
|
92
|
+
val remote = userApi.getUsers()
|
|
93
|
+
userDao.upsertAll(remote.map(userMapper::toEntity))
|
|
94
|
+
prefs.setLastSyncTime(clock.instant())
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## In-Memory Cache
|
|
102
|
+
|
|
103
|
+
```kotlin
|
|
104
|
+
// ✅ Simple in-memory LRU cache for expensive computations
|
|
105
|
+
class ImageThumbnailCache {
|
|
106
|
+
private val cache = object : LruCache<String, Bitmap>(
|
|
107
|
+
(Runtime.getRuntime().maxMemory() / 1024 / 8).toInt() // 1/8 of available memory
|
|
108
|
+
) {
|
|
109
|
+
override fun sizeOf(key: String, value: Bitmap): Int =
|
|
110
|
+
value.byteCount / 1024
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
fun get(key: String): Bitmap? = cache.get(key)
|
|
114
|
+
fun put(key: String, bitmap: Bitmap) = cache.put(key, bitmap)
|
|
115
|
+
fun evict(key: String) = cache.remove(key)
|
|
116
|
+
fun clear() = cache.evictAll()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ✅ Simple in-memory cache with TTL
|
|
120
|
+
class InMemoryCache<K, V>(private val ttlMs: Long = 5 * 60 * 1000L) {
|
|
121
|
+
private data class Entry<V>(val value: V, val timestamp: Long)
|
|
122
|
+
private val store = ConcurrentHashMap<K, Entry<V>>()
|
|
123
|
+
|
|
124
|
+
fun get(key: K): V? {
|
|
125
|
+
val entry = store[key] ?: return null
|
|
126
|
+
return if (System.currentTimeMillis() - entry.timestamp < ttlMs) {
|
|
127
|
+
entry.value
|
|
128
|
+
} else {
|
|
129
|
+
store.remove(key)
|
|
130
|
+
null
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fun put(key: K, value: V) {
|
|
135
|
+
store[key] = Entry(value, System.currentTimeMillis())
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun invalidate(key: K) = store.remove(key)
|
|
139
|
+
fun invalidateAll() = store.clear()
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Network Cache with OkHttp
|
|
146
|
+
|
|
147
|
+
```kotlin
|
|
148
|
+
// ✅ OkHttp disk cache — caches HTTP responses
|
|
149
|
+
val cacheDir = File(context.cacheDir, "http_cache")
|
|
150
|
+
val cache = Cache(cacheDir, 10L * 1024 * 1024) // 10 MB
|
|
151
|
+
|
|
152
|
+
val okHttpClient = OkHttpClient.Builder()
|
|
153
|
+
.cache(cache)
|
|
154
|
+
.addInterceptor { chain ->
|
|
155
|
+
val request = chain.request().newBuilder()
|
|
156
|
+
.header("Cache-Control", "max-age=300") // 5 min cache
|
|
157
|
+
.build()
|
|
158
|
+
chain.proceed(request)
|
|
159
|
+
}
|
|
160
|
+
.addNetworkInterceptor { chain ->
|
|
161
|
+
val response = chain.proceed(chain.request())
|
|
162
|
+
response.newBuilder()
|
|
163
|
+
.header("Cache-Control", "public, max-age=300")
|
|
164
|
+
.build()
|
|
165
|
+
}
|
|
166
|
+
.build()
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Cache Invalidation
|
|
172
|
+
|
|
173
|
+
```kotlin
|
|
174
|
+
// ✅ Invalidate on write — always update cache on mutation
|
|
175
|
+
class UserRepository @Inject constructor(
|
|
176
|
+
private val userDao: UserDao,
|
|
177
|
+
private val userApi: UserApi
|
|
178
|
+
) {
|
|
179
|
+
// After creating a user — refresh local cache
|
|
180
|
+
suspend fun createUser(user: User): Result<User> = runCatching {
|
|
181
|
+
val created = userApi.createUser(user.toDto())
|
|
182
|
+
userDao.upsert(created.toEntity()) // cache is updated
|
|
183
|
+
created.toDomain()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// After deleting — remove from cache
|
|
187
|
+
suspend fun deleteUser(userId: String): Result<Unit> = runCatching {
|
|
188
|
+
userApi.deleteUser(userId)
|
|
189
|
+
userDao.deleteById(userId) // remove from local cache
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ✅ Force refresh — ignore cache
|
|
194
|
+
suspend fun forceRefresh() {
|
|
195
|
+
val remote = userApi.getUsers()
|
|
196
|
+
userDao.deleteAll()
|
|
197
|
+
userDao.upsertAll(remote.map { it.toEntity() })
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Pull-to-Refresh Pattern
|
|
204
|
+
|
|
205
|
+
```kotlin
|
|
206
|
+
@HiltViewModel
|
|
207
|
+
class UserListViewModel @Inject constructor(
|
|
208
|
+
private val repository: UserRepository
|
|
209
|
+
) : ViewModel() {
|
|
210
|
+
|
|
211
|
+
val users: StateFlow<List<User>> = repository.observeUsers()
|
|
212
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
|
213
|
+
|
|
214
|
+
private val _isRefreshing = MutableStateFlow(false)
|
|
215
|
+
val isRefreshing: StateFlow<Boolean> = _isRefreshing.asStateFlow()
|
|
216
|
+
|
|
217
|
+
fun onRefresh() {
|
|
218
|
+
viewModelScope.launch {
|
|
219
|
+
_isRefreshing.value = true
|
|
220
|
+
repository.refreshUsers()
|
|
221
|
+
_isRefreshing.value = false
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Compose UI
|
|
227
|
+
@Composable
|
|
228
|
+
fun UserListScreen(viewModel: UserListViewModel = hiltViewModel()) {
|
|
229
|
+
val users by viewModel.users.collectAsStateWithLifecycle()
|
|
230
|
+
val isRefreshing by viewModel.isRefreshing.collectAsStateWithLifecycle()
|
|
231
|
+
|
|
232
|
+
SwipeRefresh(
|
|
233
|
+
state = rememberSwipeRefreshState(isRefreshing),
|
|
234
|
+
onRefresh = viewModel::onRefresh
|
|
235
|
+
) {
|
|
236
|
+
UserList(users = users)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Anti-Patterns
|
|
244
|
+
|
|
245
|
+
- Caching in ViewModel — lost on rotation or process death
|
|
246
|
+
- No TTL on cache — serves stale data indefinitely
|
|
247
|
+
- Caching sensitive data (tokens, PII) in plaintext — use EncryptedSharedPreferences or keystore
|
|
248
|
+
- No cache invalidation on write — UI shows stale data after mutations
|
|
249
|
+
- Caching everything — large objects in memory cause OOM
|
|
250
|
+
- No force-refresh mechanism — users can't recover from stale state
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Related Skills
|
|
255
|
+
|
|
256
|
+
- `repository-pattern` — cache lives in repository layer
|
|
257
|
+
- `room` — Room as the offline-first cache
|
|
258
|
+
- `datastore` — caching simple values across sessions
|
|
259
|
+
- `offline-first` — full offline-first architecture
|
|
260
|
+
- `okhttp` — HTTP-level caching
|
|
261
|
+
- `key-value-store-strategy` — choosing the right cache storage
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conflict-resolution
|
|
3
|
+
description: >
|
|
4
|
+
Conflict resolution strategies for Android offline-first sync.
|
|
5
|
+
Load this skill when local and remote versions of the same data diverge,
|
|
6
|
+
designing conflict detection, or choosing a resolution strategy.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Conflict Resolution
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
A conflict occurs when the same data is modified in both local storage and the remote server before a sync. Conflicts are inevitable in offline-first apps. A conflict resolution strategy defines which version wins — or how to merge them.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Core Principles
|
|
17
|
+
|
|
18
|
+
- **Detect conflicts explicitly** — don't silently overwrite
|
|
19
|
+
- Choose a strategy appropriate to the data type — not all data needs the same policy
|
|
20
|
+
- **Last-Write-Wins (LWW)** is simple but lossy — use where data loss is acceptable
|
|
21
|
+
- **Server-wins** is safe for critical shared data — client changes are discarded
|
|
22
|
+
- **Merge** is complex but preserves both sides — use for collaborative data
|
|
23
|
+
- Always log conflicts — they reveal sync bugs and UX pain points
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Conflict Detection
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Track version or timestamp on every entity
|
|
31
|
+
@Entity(tableName = "users")
|
|
32
|
+
data class UserEntity(
|
|
33
|
+
@PrimaryKey val id: String,
|
|
34
|
+
val name: String,
|
|
35
|
+
val email: String,
|
|
36
|
+
@ColumnInfo(name = "updated_at") val updatedAt: Long, // local modification time
|
|
37
|
+
@ColumnInfo(name = "server_version") val serverVersion: Int, // server's version counter
|
|
38
|
+
@ColumnInfo(name = "sync_state") val syncState: String = "SYNCED"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// ✅ Detect conflict: local changed AND server changed since last sync
|
|
42
|
+
data class ConflictCandidate(
|
|
43
|
+
val local: UserEntity,
|
|
44
|
+
val remote: UserDto
|
|
45
|
+
) {
|
|
46
|
+
val isConflict: Boolean
|
|
47
|
+
get() = local.syncState == "UPDATED" &&
|
|
48
|
+
remote.serverVersion > local.serverVersion
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Strategy 1: Last-Write-Wins (LWW)
|
|
55
|
+
|
|
56
|
+
```kotlin
|
|
57
|
+
// ✅ Simple — most recent timestamp wins
|
|
58
|
+
// Use for: user preferences, settings, non-critical data
|
|
59
|
+
|
|
60
|
+
class LWWConflictResolver {
|
|
61
|
+
fun resolve(local: UserEntity, remote: UserDto): UserEntity {
|
|
62
|
+
return if (remote.updatedAt >= local.updatedAt) {
|
|
63
|
+
// Remote wins — overwrite local
|
|
64
|
+
remote.toEntity()
|
|
65
|
+
} else {
|
|
66
|
+
// Local wins — keep local, mark for push
|
|
67
|
+
local.copy(syncState = "UPDATED")
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Strategy 2: Server-Wins
|
|
76
|
+
|
|
77
|
+
```kotlin
|
|
78
|
+
// ✅ Server is always authoritative
|
|
79
|
+
// Use for: financial data, inventory, shared resources, compliance-sensitive data
|
|
80
|
+
|
|
81
|
+
class ServerWinsConflictResolver {
|
|
82
|
+
fun resolve(local: UserEntity, remote: UserDto): UserEntity {
|
|
83
|
+
// Always use server version — discard local changes
|
|
84
|
+
return remote.toEntity().copy(syncState = "SYNCED")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Strategy 3: Client-Wins
|
|
92
|
+
|
|
93
|
+
```kotlin
|
|
94
|
+
// ✅ Local changes always take priority — push to server
|
|
95
|
+
// Use for: user-authored content, personal notes, drafts
|
|
96
|
+
|
|
97
|
+
class ClientWinsConflictResolver {
|
|
98
|
+
fun resolve(local: UserEntity, remote: UserDto): UserEntity {
|
|
99
|
+
// Keep local — it will be pushed to server
|
|
100
|
+
return local.copy(
|
|
101
|
+
serverVersion = remote.serverVersion, // update version tracking
|
|
102
|
+
syncState = "UPDATED" // mark for push
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Strategy 4: Field-Level Merge
|
|
111
|
+
|
|
112
|
+
```kotlin
|
|
113
|
+
// ✅ Merge non-overlapping field changes
|
|
114
|
+
// Use for: collaborative editing, profile data with independent fields
|
|
115
|
+
|
|
116
|
+
class FieldMergeConflictResolver {
|
|
117
|
+
|
|
118
|
+
fun resolve(base: UserEntity, local: UserEntity, remote: UserDto): UserEntity {
|
|
119
|
+
// Three-way merge: base (last synced) + local changes + remote changes
|
|
120
|
+
return UserEntity(
|
|
121
|
+
id = local.id,
|
|
122
|
+
// If only one side changed a field, take that change
|
|
123
|
+
// If both changed the same field → apply strategy (LWW, server-wins, etc.)
|
|
124
|
+
name = mergeField(
|
|
125
|
+
base = base.name,
|
|
126
|
+
local = local.name,
|
|
127
|
+
remote = remote.name,
|
|
128
|
+
onConflict = { _, remoteVal -> remoteVal } // server-wins for name
|
|
129
|
+
),
|
|
130
|
+
email = mergeField(
|
|
131
|
+
base = base.email,
|
|
132
|
+
local = local.email,
|
|
133
|
+
remote = remote.email,
|
|
134
|
+
onConflict = { localVal, _ -> localVal } // client-wins for email
|
|
135
|
+
),
|
|
136
|
+
updatedAt = maxOf(local.updatedAt, remote.updatedAt),
|
|
137
|
+
serverVersion = remote.serverVersion,
|
|
138
|
+
syncState = "SYNCED"
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private fun <T> mergeField(
|
|
143
|
+
base: T,
|
|
144
|
+
local: T,
|
|
145
|
+
remote: T,
|
|
146
|
+
onConflict: (T, T) -> T
|
|
147
|
+
): T {
|
|
148
|
+
val localChanged = local != base
|
|
149
|
+
val remoteChanged = remote != base
|
|
150
|
+
return when {
|
|
151
|
+
localChanged && !remoteChanged -> local
|
|
152
|
+
!localChanged && remoteChanged -> remote
|
|
153
|
+
localChanged && remoteChanged -> onConflict(local, remote)
|
|
154
|
+
else -> base // neither changed
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Strategy 5: User-Prompted Resolution
|
|
163
|
+
|
|
164
|
+
```kotlin
|
|
165
|
+
// ✅ Let the user decide — for important data where automated resolution is risky
|
|
166
|
+
|
|
167
|
+
sealed class ConflictResolutionChoice {
|
|
168
|
+
object KeepLocal : ConflictResolutionChoice()
|
|
169
|
+
object KeepRemote : ConflictResolutionChoice()
|
|
170
|
+
data class Merge(val merged: User) : ConflictResolutionChoice()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// In ViewModel — emit conflict event
|
|
174
|
+
data class ConflictData(val local: User, val remote: User)
|
|
175
|
+
|
|
176
|
+
sealed class SyncEvent {
|
|
177
|
+
data class ConflictDetected(val conflict: ConflictData) : SyncEvent()
|
|
178
|
+
object SyncComplete : SyncEvent()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
@HiltViewModel
|
|
182
|
+
class SyncViewModel @Inject constructor(
|
|
183
|
+
private val syncEngine: UserSyncEngine
|
|
184
|
+
) : ViewModel() {
|
|
185
|
+
|
|
186
|
+
private val _events = MutableSharedFlow<SyncEvent>()
|
|
187
|
+
val events: SharedFlow<SyncEvent> = _events.asSharedFlow()
|
|
188
|
+
|
|
189
|
+
fun resolveConflict(conflict: ConflictData, choice: ConflictResolutionChoice) {
|
|
190
|
+
viewModelScope.launch {
|
|
191
|
+
syncEngine.applyResolution(conflict, choice)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Conflict Logging
|
|
200
|
+
|
|
201
|
+
```kotlin
|
|
202
|
+
// ✅ Log every conflict for debugging and analytics
|
|
203
|
+
class ConflictLogger @Inject constructor(private val logger: Logger) {
|
|
204
|
+
|
|
205
|
+
fun log(local: UserEntity, remote: UserDto, resolution: String) {
|
|
206
|
+
logger.info(
|
|
207
|
+
tag = "ConflictResolution",
|
|
208
|
+
message = buildString {
|
|
209
|
+
append("Conflict for user ${local.id}: ")
|
|
210
|
+
append("local updated_at=${local.updatedAt}, ")
|
|
211
|
+
append("remote updated_at=${remote.updatedAt}, ")
|
|
212
|
+
append("resolution=$resolution")
|
|
213
|
+
}
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Strategy Selection Guide
|
|
222
|
+
|
|
223
|
+
| Data Type | Recommended Strategy |
|
|
224
|
+
|-----------|---------------------|
|
|
225
|
+
| User settings / preferences | Last-Write-Wins |
|
|
226
|
+
| Financial transactions | Server-wins |
|
|
227
|
+
| User-authored content (notes, posts) | Client-wins or User-prompted |
|
|
228
|
+
| Shared collaborative data | Field-level merge |
|
|
229
|
+
| Inventory / stock levels | Server-wins |
|
|
230
|
+
| Profile with independent fields | Field-level merge |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Anti-Patterns
|
|
235
|
+
|
|
236
|
+
- No conflict detection — silently overwriting data, data loss goes unnoticed
|
|
237
|
+
- Always server-wins for user-authored content — users lose their work
|
|
238
|
+
- Always client-wins for shared data — other users' changes are lost
|
|
239
|
+
- Resolving conflicts on the main thread — always use Dispatchers.IO
|
|
240
|
+
- Not logging conflicts — impossible to diagnose sync bugs in production
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Related Skills
|
|
245
|
+
- `sync-engine` — sync architecture and scheduling
|
|
246
|
+
- `merge-strategy` — merging list/collection changes
|
|
247
|
+
- `room` — local storage for conflict tracking
|
|
248
|
+
- `workmanager` — background sync execution
|