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,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
|