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.
Files changed (176) hide show
  1. package/dist/index.js +143 -0
  2. package/package.json +27 -0
  3. package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
  4. package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
  5. package/skills/Android Platform/Configuration/SKILL.md +201 -0
  6. package/skills/Android Platform/Filesystem/SKILL.md +216 -0
  7. package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
  8. package/skills/Android Platform/Manifest/SKILL.md +226 -0
  9. package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
  10. package/skills/Android Platform/Resources/SKILL.md +234 -0
  11. package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
  12. package/skills/Android Platform/State Restoration/SKILL.md +210 -0
  13. package/skills/Architecture/Bounded Context/SKILL.md +207 -0
  14. package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
  15. package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
  16. package/skills/Architecture/Entity Design/SKILL.md +243 -0
  17. package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
  18. package/skills/Architecture/MVI/SKILL.md +224 -0
  19. package/skills/Architecture/MVVM/SKILL.md +198 -0
  20. package/skills/Architecture/Modularization/SKILL.md +194 -0
  21. package/skills/Architecture/Offline First/SKILL.md +249 -0
  22. package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
  23. package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
  24. package/skills/Architecture/State Management/SKILL.md +229 -0
  25. package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
  26. package/skills/Architecture/Use Case Design/SKILL.md +244 -0
  27. package/skills/Architecture/Value Object/SKILL.md +226 -0
  28. package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
  29. package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
  30. package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
  31. package/skills/Build System/Build Cache/SKILL.md +233 -0
  32. package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
  33. package/skills/Build System/Build Variant/SKILL.md +215 -0
  34. package/skills/Build System/Convention Plugin/SKILL.md +288 -0
  35. package/skills/Build System/Dependency Management/SKILL.md +261 -0
  36. package/skills/Build System/Gradle/SKILL.md +284 -0
  37. package/skills/Build System/Incremental Build/SKILL.md +199 -0
  38. package/skills/Build System/KAPT/SKILL.md +198 -0
  39. package/skills/Build System/KSP/SKILL.md +263 -0
  40. package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
  41. package/skills/Build System/Specialized/C++/SKILL.md +308 -0
  42. package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
  43. package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
  44. package/skills/Build System/Version Catalog/SKILL.md +304 -0
  45. package/skills/Concurrency/Background Processing/SKILL.md +185 -0
  46. package/skills/Concurrency/Channel/SKILL.md +207 -0
  47. package/skills/Concurrency/Coroutine/SKILL.md +200 -0
  48. package/skills/Concurrency/Flow/SKILL.md +179 -0
  49. package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
  50. package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
  51. package/skills/Concurrency/StateFlow/SKILL.md +175 -0
  52. package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
  53. package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
  54. package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
  55. package/skills/Core Language/DSL/SKILL.md +186 -0
  56. package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
  57. package/skills/Core Language/Immutability/SKILL.md +156 -0
  58. package/skills/Core Language/KMP/SKILL.md +182 -0
  59. package/skills/Core Language/Kotlin/SKILL.md +187 -0
  60. package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
  61. package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
  62. package/skills/Core Language/Serialization/SKILL.md +191 -0
  63. package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
  64. package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
  65. package/skills/Data Layer/DAO/SKILL.md +225 -0
  66. package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
  67. package/skills/Data Layer/DataStore/SKILL.md +264 -0
  68. package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
  69. package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
  70. package/skills/Data Layer/File Storage/SKILL.md +247 -0
  71. package/skills/Data Layer/Indexing/SKILL.md +184 -0
  72. package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
  73. package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
  74. package/skills/Data Layer/Migration/SKILL.md +243 -0
  75. package/skills/Data Layer/Paging/SKILL.md +264 -0
  76. package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
  77. package/skills/Data Layer/Room/SKILL.md +244 -0
  78. package/skills/Data Layer/SQLite/SKILL.md +255 -0
  79. package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
  80. package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
  81. package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
  82. package/skills/Dependency Injection/Koin/SKILL.md +282 -0
  83. package/skills/Developer Experience/Detekt/SKILL.md +272 -0
  84. package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
  85. package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
  86. package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
  87. package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
  88. package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
  89. package/skills/Media/Audio/SKILL.md +257 -0
  90. package/skills/Media/Camera/SKILL.md +229 -0
  91. package/skills/Media/CameraX/SKILL.md +295 -0
  92. package/skills/Media/ExoPlayer/SKILL.md +258 -0
  93. package/skills/Media/Video/SKILL.md +228 -0
  94. package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
  95. package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
  96. package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
  97. package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
  98. package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
  99. package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
  100. package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
  101. package/skills/Navigation/Navigation/SKILL.md +215 -0
  102. package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
  103. package/skills/Networking/API Contract/SKILL.md +220 -0
  104. package/skills/Networking/Authentication/SKILL.md +210 -0
  105. package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
  106. package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
  107. package/skills/Networking/Ktor/SKILL.md +219 -0
  108. package/skills/Networking/Multipart Upload/SKILL.md +213 -0
  109. package/skills/Networking/OkHttp/SKILL.md +193 -0
  110. package/skills/Networking/REST/SKILL.md +178 -0
  111. package/skills/Networking/Rate Limiting/SKILL.md +170 -0
  112. package/skills/Networking/Retrofit/SKILL.md +241 -0
  113. package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
  114. package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
  115. package/skills/Networking/WebSocket/SKILL.md +224 -0
  116. package/skills/Observability/Crash Reporting/SKILL.md +219 -0
  117. package/skills/Observability/Logging/SKILL.md +168 -0
  118. package/skills/Observability/Metrics/SKILL.md +227 -0
  119. package/skills/Observability/Structured Logging/SKILL.md +234 -0
  120. package/skills/Performance/ANR Prevention/SKILL.md +192 -0
  121. package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
  122. package/skills/Performance/App Startup/SKILL.md +183 -0
  123. package/skills/Performance/Baseline Profile/SKILL.md +205 -0
  124. package/skills/Performance/Battery Optimization/SKILL.md +192 -0
  125. package/skills/Performance/Benchmark/SKILL.md +182 -0
  126. package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
  127. package/skills/Performance/Compose Optimization/SKILL.md +187 -0
  128. package/skills/Performance/Heap Management/SKILL.md +184 -0
  129. package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
  130. package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
  131. package/skills/Performance/Rendering Performance/SKILL.md +205 -0
  132. package/skills/Performance/Startup Optimization/SKILL.md +219 -0
  133. package/skills/Security/Biometric/SKILL.md +224 -0
  134. package/skills/Security/Certificate Transparency/SKILL.md +158 -0
  135. package/skills/Security/Cryptography/SKILL.md +244 -0
  136. package/skills/Security/Encrypted Storage/SKILL.md +273 -0
  137. package/skills/Security/Frida Detection/SKILL.md +230 -0
  138. package/skills/Security/Hook Detection/SKILL.md +197 -0
  139. package/skills/Security/Keystore/SKILL.md +272 -0
  140. package/skills/Security/Network Security Config/SKILL.md +186 -0
  141. package/skills/Security/Obfuscation/SKILL.md +226 -0
  142. package/skills/Security/Proguard/SKILL.md +202 -0
  143. package/skills/Security/R8/SKILL.md +234 -0
  144. package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
  145. package/skills/Security/Root Detection/SKILL.md +220 -0
  146. package/skills/Security/Secure Networking/SKILL.md +220 -0
  147. package/skills/System Integration/AlarmManager/SKILL.md +182 -0
  148. package/skills/System Integration/App Widget/SKILL.md +182 -0
  149. package/skills/System Integration/Deep Link/SKILL.md +187 -0
  150. package/skills/System Integration/Foreground Service/SKILL.md +212 -0
  151. package/skills/System Integration/Notification/SKILL.md +237 -0
  152. package/skills/System Integration/WorkManager/SKILL.md +256 -0
  153. package/skills/System Integration/clipboard/SKILL.md +155 -0
  154. package/skills/System Integration/share-intent/SKILL.md +182 -0
  155. package/skills/Testing/Compose Testing/SKILL.md +296 -0
  156. package/skills/Testing/Espresso/SKILL.md +292 -0
  157. package/skills/Testing/Fake Data/SKILL.md +245 -0
  158. package/skills/Testing/Integration Testing/SKILL.md +288 -0
  159. package/skills/Testing/Mocking/SKILL.md +229 -0
  160. package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
  161. package/skills/Testing/UI Testing/SKILL.md +293 -0
  162. package/skills/Testing/Unit Testing/SKILL.md +309 -0
  163. package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
  164. package/skills/UI System/Compose/SKILL.md +296 -0
  165. package/skills/UI System/Compose Animation/SKILL.md +281 -0
  166. package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
  167. package/skills/UI System/Compose Navigation/SKILL.md +255 -0
  168. package/skills/UI System/Compose Performance/SKILL.md +274 -0
  169. package/skills/UI System/Design System/SKILL.md +217 -0
  170. package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
  171. package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
  172. package/skills/UI System/Loading Strategy/SKILL.md +254 -0
  173. package/skills/UI System/Material 3/SKILL.md +279 -0
  174. package/skills/UI System/RTL/SKILL.md +179 -0
  175. package/src/index.ts +182 -0
  176. package/tsconfig.json +19 -0
@@ -0,0 +1,232 @@
1
+ ---
2
+ name: error-mapping
3
+ description: >
4
+ Error mapping between layers in Android Clean Architecture.
5
+ Load this skill when converting HTTP exceptions to domain errors,
6
+ mapping Room/SQLite exceptions to domain errors, translating domain
7
+ errors to UI messages, or designing the error translation pipeline.
8
+ ---
9
+
10
+ # Error Mapping
11
+
12
+ ## Overview
13
+ Error mapping converts layer-specific errors (HTTP status codes, Room exceptions, IO errors) into domain errors at each architectural boundary. The domain layer defines a typed error hierarchy; the data layer maps raw exceptions to it; the presentation layer maps domain errors to user-facing messages.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - **Map at boundaries** — data layer maps to domain errors; ViewModel maps to messages
20
+ - **Domain errors know nothing** about HTTP, Room, or Retrofit
21
+ - **Exhaustive mapping** — every possible error type has an explicit mapping
22
+ - **Preserve context** — include enough info in domain errors to display meaningful messages
23
+ - **One mapper per data source** — separate HTTP mapper, Room mapper, etc.
24
+
25
+ ---
26
+
27
+ ## Domain Error Model
28
+
29
+ ```kotlin
30
+ // ✅ Domain errors — pure Kotlin, no framework imports
31
+ // (See domain-error-model skill for full hierarchy)
32
+ sealed interface AppError {
33
+ // Network
34
+ sealed interface Network : AppError {
35
+ data object NoConnection : Network
36
+ data object Timeout : Network
37
+ data class ServerError(val code: Int) : Network
38
+ data object Unauthorized : Network
39
+ data object Forbidden : Network
40
+ data object NotFound : Network
41
+ }
42
+ // Data
43
+ sealed interface Data : AppError {
44
+ data object NotFound : Data
45
+ data class Conflict(val field: String) : Data
46
+ data class Validation(val errors: Map<String, String>) : Data
47
+ }
48
+ // Local storage
49
+ sealed interface Storage : AppError {
50
+ data object DiskFull : Storage
51
+ data object Corrupted : Storage
52
+ data class Unknown(val cause: Throwable) : Storage
53
+ }
54
+ // Unknown
55
+ data class Unexpected(val cause: Throwable) : AppError
56
+ }
57
+ ```
58
+
59
+ ---
60
+
61
+ ## HTTP Error Mapper
62
+
63
+ ```kotlin
64
+ // ✅ Map Retrofit/HTTP exceptions to domain errors
65
+ object HttpErrorMapper {
66
+
67
+ fun map(throwable: Throwable): AppError = when (throwable) {
68
+ is HttpException -> mapHttpException(throwable)
69
+ is IOException -> mapIoException(throwable)
70
+ is SSLException -> AppError.Network.NoConnection
71
+ else -> AppError.Unexpected(throwable)
72
+ }
73
+
74
+ private fun mapHttpException(e: HttpException): AppError = when (e.code()) {
75
+ 400 -> parseValidationError(e) ?: AppError.Unexpected(e)
76
+ 401 -> AppError.Network.Unauthorized
77
+ 403 -> AppError.Network.Forbidden
78
+ 404 -> AppError.Network.NotFound
79
+ 409 -> parseConflictError(e) ?: AppError.Data.Conflict("unknown")
80
+ in 500..599 -> AppError.Network.ServerError(e.code())
81
+ else -> AppError.Unexpected(e)
82
+ }
83
+
84
+ private fun mapIoException(e: IOException): AppError = when (e) {
85
+ is SocketTimeoutException,
86
+ is ConnectTimeoutException -> AppError.Network.Timeout
87
+ else -> AppError.Network.NoConnection
88
+ }
89
+
90
+ private fun parseValidationError(e: HttpException): AppError? = try {
91
+ val body = e.response()?.errorBody()?.string() ?: return null
92
+ val errors = Json.decodeFromString<ValidationErrorDto>(body)
93
+ AppError.Data.Validation(errors.fields)
94
+ } catch (parseException: Exception) {
95
+ null
96
+ }
97
+
98
+ private fun parseConflictError(e: HttpException): AppError? = try {
99
+ val body = e.response()?.errorBody()?.string() ?: return null
100
+ val error = Json.decodeFromString<ConflictErrorDto>(body)
101
+ AppError.Data.Conflict(error.field)
102
+ } catch (parseException: Exception) {
103
+ null
104
+ }
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Room Error Mapper
111
+
112
+ ```kotlin
113
+ // ✅ Map Room/SQLite exceptions to domain errors
114
+ object RoomErrorMapper {
115
+
116
+ fun map(throwable: Throwable): AppError = when (throwable) {
117
+ is SQLiteFullException -> AppError.Storage.DiskFull
118
+ is SQLiteDatabaseCorruptException -> AppError.Storage.Corrupted
119
+ is SQLiteConstraintException -> parseConstraintError(throwable)
120
+ else -> AppError.Storage.Unknown(throwable)
121
+ }
122
+
123
+ private fun parseConstraintError(e: SQLiteConstraintException): AppError {
124
+ val message = e.message ?: ""
125
+ return when {
126
+ message.contains("UNIQUE") -> AppError.Data.Conflict(
127
+ extractColumnName(message)
128
+ )
129
+ else -> AppError.Storage.Unknown(e)
130
+ }
131
+ }
132
+
133
+ private fun extractColumnName(message: String): String {
134
+ // "UNIQUE constraint failed: users.email" → "email"
135
+ return message.substringAfterLast(".").takeIf { it.isNotBlank() } ?: "unknown"
136
+ }
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Repository Using Mappers
143
+
144
+ ```kotlin
145
+ // ✅ Repository applies mappers at the boundary
146
+ class UserRepositoryImpl @Inject constructor(
147
+ private val api: UserApiService,
148
+ private val dao: UserDao
149
+ ) : UserRepository {
150
+
151
+ override suspend fun getUser(id: String): Result<User> =
152
+ runCatching {
153
+ dao.getById(id)?.toDomain()
154
+ ?: api.getUser(id).toDomain().also { dao.insert(it.toEntity()) }
155
+ }.mapFailure { throwable ->
156
+ when (throwable) {
157
+ is HttpException,
158
+ is IOException -> HttpErrorMapper.map(throwable)
159
+ is SQLiteException -> RoomErrorMapper.map(throwable)
160
+ else -> AppError.Unexpected(throwable)
161
+ }
162
+ }
163
+
164
+ override suspend fun createUser(user: User): Result<User> =
165
+ runCatching {
166
+ api.createUser(user.toCreateRequest()).toDomain()
167
+ }.mapFailure { HttpErrorMapper.map(it) }
168
+ }
169
+
170
+ // ✅ Extension to map the failure in a Result
171
+ fun <T> Result<T>.mapFailure(transform: (Throwable) -> Throwable): Result<T> =
172
+ fold(
173
+ onSuccess = { Result.success(it) },
174
+ onFailure = { Result.failure(transform(it)) }
175
+ )
176
+
177
+ // Or use domain error as sealed class (not Throwable)
178
+ fun <T> Result<T>.mapError(transform: (Throwable) -> AppError): Result<T> =
179
+ recoverCatching { throw AppErrorException(transform(it)) }
180
+
181
+ class AppErrorException(val error: AppError) : Exception()
182
+ ```
183
+
184
+ ---
185
+
186
+ ## ViewModel → UI Message Mapping
187
+
188
+ ```kotlin
189
+ // ✅ ViewModel maps domain errors to UI strings
190
+ fun AppError.toUiMessage(context: Context): String = when (this) {
191
+ is AppError.Network.NoConnection -> context.getString(R.string.error_no_connection)
192
+ is AppError.Network.Timeout -> context.getString(R.string.error_timeout)
193
+ is AppError.Network.Unauthorized -> context.getString(R.string.error_session_expired)
194
+ is AppError.Network.Forbidden -> context.getString(R.string.error_no_permission)
195
+ is AppError.Network.NotFound -> context.getString(R.string.error_not_found)
196
+ is AppError.Network.ServerError -> context.getString(R.string.error_server, code)
197
+ is AppError.Data.NotFound -> context.getString(R.string.error_item_not_found)
198
+ is AppError.Data.Conflict -> context.getString(R.string.error_conflict, field)
199
+ is AppError.Data.Validation -> errors.values.first()
200
+ is AppError.Storage.DiskFull -> context.getString(R.string.error_disk_full)
201
+ is AppError.Storage.Corrupted -> context.getString(R.string.error_data_corrupted)
202
+ is AppError.Storage.Unknown,
203
+ is AppError.Unexpected -> context.getString(R.string.error_unexpected)
204
+ }
205
+
206
+ // ✅ Or without Context — use string keys resolved in Compose
207
+ fun AppError.toMessageKey(): Int = when (this) {
208
+ is AppError.Network.NoConnection -> R.string.error_no_connection
209
+ is AppError.Network.Timeout -> R.string.error_timeout
210
+ // ...
211
+ else -> R.string.error_unexpected
212
+ }
213
+ ```
214
+
215
+ ---
216
+
217
+ ## Anti-Patterns
218
+
219
+ - Mapping HTTP codes in the ViewModel — belongs in the data layer mapper
220
+ - Domain error types that import `retrofit2` or `androidx.room` — domain must stay pure
221
+ - Catch-all `else -> AppError.Unexpected` without logging — unexpected errors should be tracked
222
+ - Showing raw exception messages to users — `e.message` is for developers, not users
223
+ - One giant `when(throwable)` across all layers — each layer has its own mapper
224
+
225
+ ---
226
+
227
+ ## Related Skills
228
+ - `error-handling` — overall error propagation strategy
229
+ - `domain-error-model` — the domain error sealed class hierarchy
230
+ - `failure-strategy` — how to respond to each error type
231
+ - `user-friendly-errors` — composable/string resource error display
232
+ - `repository-pattern` — where mappers are applied
@@ -0,0 +1,294 @@
1
+ ---
2
+ name: failure-strategy
3
+ description: >
4
+ Failure response strategy for Android apps.
5
+ Load this skill when deciding how to respond to different error types,
6
+ implementing retry logic, handling session expiry globally,
7
+ designing fallback behavior, or choosing between silent fail and user notification.
8
+ ---
9
+
10
+ # Failure Strategy
11
+
12
+ ## Overview
13
+ Failure strategy defines what the app does when an error occurs — not just what message to show, but whether to retry, fall back to cached data, redirect to login, or block the user. Different errors require different responses, and some responses (like session expiry handling) should be centralized rather than handled per-screen.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - **Match response to error severity** — silent fail for non-critical, block for auth failures
20
+ - **Centralize cross-cutting failures** — session expiry and network availability are app-level concerns
21
+ - **Retry with backoff** for transient errors — network timeouts and server errors
22
+ - **Fallback to cache** when possible — offline-first is better than blank error screens
23
+ - **Let the user decide** on recoverable errors — show retry, not just error message
24
+
25
+ ---
26
+
27
+ ## Failure Response Matrix
28
+
29
+ | Error | Response |
30
+ |---|---|
31
+ | `Network.NoConnection` | Show offline banner + cached data |
32
+ | `Network.Timeout` | Auto-retry (1-2x), then show retry button |
33
+ | `Network.ServerError` | Show error + retry button |
34
+ | `Network.Unauthorized` | Redirect to login (global handler) |
35
+ | `Network.Forbidden` | Show permission error, no retry |
36
+ | `Network.NotFound` | Show not-found state, no retry |
37
+ | `Data.Validation` | Show inline field errors |
38
+ | `Data.Conflict` | Show specific conflict message |
39
+ | `Storage.DiskFull` | Show disk full warning, prompt to free space |
40
+ | `Auth.AccountSuspended` | Show suspension message, no retry |
41
+ | `Unexpected` | Log + show generic error + retry |
42
+
43
+ ---
44
+
45
+ ## Retry with Exponential Backoff
46
+
47
+ ```kotlin
48
+ // ✅ Retry helper for transient errors
49
+ suspend fun <T> retryWithBackoff(
50
+ times: Int = 3,
51
+ initialDelay: Long = 500L,
52
+ maxDelay: Long = 5_000L,
53
+ factor: Double = 2.0,
54
+ retryIf: (Throwable) -> Boolean = { true },
55
+ block: suspend () -> T
56
+ ): T {
57
+ var currentDelay = initialDelay
58
+ repeat(times - 1) { attempt ->
59
+ try {
60
+ return block()
61
+ } catch (e: CancellationException) {
62
+ throw e // never retry cancellation
63
+ } catch (e: Exception) {
64
+ if (!retryIf(e)) throw e
65
+ Timber.w(e, "Attempt ${attempt + 1} failed, retrying in ${currentDelay}ms")
66
+ delay(currentDelay)
67
+ currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
68
+ }
69
+ }
70
+ return block() // last attempt — let it throw
71
+ }
72
+
73
+ // ✅ Usage in Repository
74
+ override suspend fun syncData(): Result<Unit> = runCatching {
75
+ retryWithBackoff(
76
+ times = 3,
77
+ retryIf = { e -> e is IOException || (e is HttpException && e.code() >= 500) }
78
+ ) {
79
+ api.syncData()
80
+ }
81
+ }
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Global Session Expiry Handler
87
+
88
+ ```kotlin
89
+ // ✅ Intercept 401 globally via OkHttp Authenticator
90
+ class TokenRefreshAuthenticator @Inject constructor(
91
+ private val securePreferences: SecurePreferences,
92
+ private val authApi: AuthApiService,
93
+ private val sessionManager: SessionManager
94
+ ) : Authenticator {
95
+
96
+ private val isRefreshing = AtomicBoolean(false)
97
+
98
+ override fun authenticate(route: Route?, response: Response): Request? {
99
+ if (response.code != 401) return null
100
+ if (responseCount(response) >= 2) {
101
+ // Refresh failed — force logout
102
+ sessionManager.logout()
103
+ return null
104
+ }
105
+
106
+ synchronized(this) {
107
+ if (isRefreshing.get()) return null
108
+ isRefreshing.set(true)
109
+
110
+ return try {
111
+ val refreshToken = securePreferences.refreshToken ?: run {
112
+ sessionManager.logout()
113
+ return null
114
+ }
115
+
116
+ val newTokens = runBlocking { authApi.refreshToken(refreshToken) }
117
+ securePreferences.authToken = newTokens.accessToken
118
+ securePreferences.refreshToken = newTokens.refreshToken
119
+
120
+ response.request.newBuilder()
121
+ .header("Authorization", "Bearer ${newTokens.accessToken}")
122
+ .build()
123
+ } catch (e: Exception) {
124
+ sessionManager.logout()
125
+ null
126
+ } finally {
127
+ isRefreshing.set(false)
128
+ }
129
+ }
130
+ }
131
+
132
+ private fun responseCount(response: Response): Int {
133
+ var count = 1
134
+ var prior = response.priorResponse
135
+ while (prior != null) { count++; prior = prior.priorResponse }
136
+ return count
137
+ }
138
+ }
139
+
140
+ // ✅ SessionManager broadcasts logout event to all screens
141
+ class SessionManager @Inject constructor() {
142
+ private val _sessionEvents = MutableSharedFlow<SessionEvent>(extraBufferCapacity = 1)
143
+ val sessionEvents: SharedFlow<SessionEvent> = _sessionEvents.asSharedFlow()
144
+
145
+ fun logout() {
146
+ _sessionEvents.tryEmit(SessionEvent.SessionExpired)
147
+ }
148
+ }
149
+
150
+ sealed interface SessionEvent {
151
+ data object SessionExpired : SessionEvent
152
+ data object LoggedOut : SessionEvent
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ ## App-Level Session Observer
159
+
160
+ ```kotlin
161
+ // ✅ Observe session events in root ViewModel or Activity
162
+ @HiltViewModel
163
+ class AppViewModel @Inject constructor(
164
+ private val sessionManager: SessionManager
165
+ ) : ViewModel() {
166
+
167
+ private val _navigationEvents = Channel<AppNavigationEvent>(Channel.BUFFERED)
168
+ val navigationEvents: Flow<AppNavigationEvent> = _navigationEvents.receiveAsFlow()
169
+
170
+ init {
171
+ viewModelScope.launch {
172
+ sessionManager.sessionEvents.collect { event ->
173
+ when (event) {
174
+ SessionEvent.SessionExpired -> {
175
+ _navigationEvents.send(AppNavigationEvent.NavigateToLogin)
176
+ }
177
+ SessionEvent.LoggedOut -> {
178
+ _navigationEvents.send(AppNavigationEvent.NavigateToLogin)
179
+ }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Fallback to Cache Strategy
190
+
191
+ ```kotlin
192
+ // ✅ Show stale cache while fetching fresh data
193
+ class ProductRepositoryImpl @Inject constructor(
194
+ private val api: ProductApiService,
195
+ private val dao: ProductDao
196
+ ) : ProductRepository {
197
+
198
+ override fun observeProducts(): Flow<List<Product>> = flow {
199
+ // 1. Emit cached data immediately
200
+ val cached = dao.getAll().map { it.toDomain() }
201
+ if (cached.isNotEmpty()) emit(cached)
202
+
203
+ // 2. Fetch fresh data in background
204
+ try {
205
+ val fresh = api.getProducts().map { it.toDomain() }
206
+ dao.replaceAll(fresh.map { it.toEntity() })
207
+ emit(fresh)
208
+ } catch (e: IOException) {
209
+ // Network failed — cached data already emitted, just log
210
+ Timber.w(e, "Failed to refresh products, showing cached data")
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
216
+ ---
217
+
218
+ ## Per-Screen Retry Pattern
219
+
220
+ ```kotlin
221
+ // ✅ Retry button in UiState
222
+ data class ProductListUiState(
223
+ val isLoading: Boolean = false,
224
+ val products: List<Product> = emptyList(),
225
+ val error: ErrorState? = null
226
+ )
227
+
228
+ data class ErrorState(
229
+ val message: String,
230
+ val canRetry: Boolean
231
+ )
232
+
233
+ // ✅ Retry composable
234
+ @Composable
235
+ fun ErrorView(
236
+ error: ErrorState,
237
+ onRetry: () -> Unit,
238
+ modifier: Modifier = Modifier
239
+ ) {
240
+ Column(
241
+ modifier = modifier,
242
+ horizontalAlignment = Alignment.CenterHorizontally
243
+ ) {
244
+ Text(error.message)
245
+ if (error.canRetry) {
246
+ Spacer(Modifier.height(16.dp))
247
+ Button(onClick = onRetry) { Text("Try Again") }
248
+ }
249
+ }
250
+ }
251
+
252
+ // ✅ ViewModel
253
+ fun loadProducts() {
254
+ viewModelScope.launch {
255
+ _state.update { it.copy(isLoading = true, error = null) }
256
+ getProductsUseCase().fold(
257
+ onSuccess = { products ->
258
+ _state.update { it.copy(isLoading = false, products = products) }
259
+ },
260
+ onFailure = { error ->
261
+ _state.update {
262
+ it.copy(
263
+ isLoading = false,
264
+ error = ErrorState(
265
+ message = error.toUserMessage(),
266
+ canRetry = (error as? AppException)?.error?.isRetryable() ?: true
267
+ )
268
+ )
269
+ }
270
+ }
271
+ )
272
+ }
273
+ }
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Anti-Patterns
279
+
280
+ - Same error response for all error types — `Unauthorized` needs redirect, `Timeout` needs retry
281
+ - Retrying non-retryable errors — `403 Forbidden` won't succeed on retry; don't offer it
282
+ - Handling session expiry in every screen — centralize in OkHttp Authenticator + AppViewModel
283
+ - Silently swallowing errors without logging — bugs become invisible
284
+ - Infinite retry without backoff — hammers the server during outages
285
+
286
+ ---
287
+
288
+ ## Related Skills
289
+ - `error-handling` — error propagation across layers
290
+ - `domain-error-model` — typed errors that drive strategy decisions
291
+ - `error-mapping` — mapping raw exceptions to domain errors
292
+ - `user-friendly-errors` — displaying error states in UI
293
+ - `offline-first` — cache fallback as part of failure strategy
294
+ - `secure-networking` — token refresh interceptor