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,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: domain-error-model
|
|
3
|
+
description: >
|
|
4
|
+
Domain error hierarchy design for Android Clean Architecture.
|
|
5
|
+
Load this skill when designing the sealed class error hierarchy,
|
|
6
|
+
deciding how granular errors should be, structuring errors by domain
|
|
7
|
+
context, or defining the contract between data and domain layers.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Domain Error Model
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
The domain error model is a sealed class hierarchy that represents all possible failure states in the application, using the language of the business — not HTTP codes or SQL exceptions. It lives in the domain layer and is the single contract between data (which produces errors) and presentation (which displays them).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- **Pure Kotlin** — no Android, Retrofit, or Room imports
|
|
20
|
+
- **Sealed hierarchy** — exhaustive, forces handling at every call site
|
|
21
|
+
- **Business language** — `Unauthorized` not `Http401`, `NotFound` not `Http404`
|
|
22
|
+
- **Granularity by need** — only as specific as the UI needs to differentiate behavior
|
|
23
|
+
- **Stable** — rarely changes; presentation adapts to it, not the other way
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Full Domain Error Hierarchy
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// domain/error/AppError.kt
|
|
31
|
+
|
|
32
|
+
sealed interface AppError {
|
|
33
|
+
|
|
34
|
+
// ── Network errors ──────────────────────────────────────────────────
|
|
35
|
+
sealed interface Network : AppError {
|
|
36
|
+
/** Device has no internet connection */
|
|
37
|
+
data object NoConnection : Network
|
|
38
|
+
|
|
39
|
+
/** Request timed out */
|
|
40
|
+
data object Timeout : Network
|
|
41
|
+
|
|
42
|
+
/** Server returned 5xx */
|
|
43
|
+
data class ServerError(val code: Int) : Network
|
|
44
|
+
|
|
45
|
+
/** 401 — session expired or token invalid */
|
|
46
|
+
data object Unauthorized : Network
|
|
47
|
+
|
|
48
|
+
/** 403 — user lacks permission */
|
|
49
|
+
data object Forbidden : Network
|
|
50
|
+
|
|
51
|
+
/** 404 — resource does not exist on server */
|
|
52
|
+
data object NotFound : Network
|
|
53
|
+
|
|
54
|
+
/** 429 — rate limited */
|
|
55
|
+
data object RateLimited : Network
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Data / business errors ───────────────────────────────────────────
|
|
59
|
+
sealed interface Data : AppError {
|
|
60
|
+
/** Requested entity does not exist locally */
|
|
61
|
+
data object NotFound : Data
|
|
62
|
+
|
|
63
|
+
/** Operation would violate a uniqueness constraint */
|
|
64
|
+
data class Conflict(val field: String) : Data
|
|
65
|
+
|
|
66
|
+
/** Input failed business validation */
|
|
67
|
+
data class Validation(val errors: Map<String, String>) : Data
|
|
68
|
+
|
|
69
|
+
/** Operation not allowed in current state */
|
|
70
|
+
data class InvalidState(val reason: String) : Data
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Storage errors ───────────────────────────────────────────────────
|
|
74
|
+
sealed interface Storage : AppError {
|
|
75
|
+
/** Device storage is full */
|
|
76
|
+
data object DiskFull : Storage
|
|
77
|
+
|
|
78
|
+
/** Local database or file is corrupted */
|
|
79
|
+
data object Corrupted : Storage
|
|
80
|
+
|
|
81
|
+
/** Unclassified storage error */
|
|
82
|
+
data class Unknown(val cause: Throwable) : Storage
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Auth / security errors ───────────────────────────────────────────
|
|
86
|
+
sealed interface Auth : AppError {
|
|
87
|
+
/** User is not authenticated */
|
|
88
|
+
data object NotAuthenticated : Auth
|
|
89
|
+
|
|
90
|
+
/** Biometric / PIN verification failed */
|
|
91
|
+
data object BiometricFailed : Auth
|
|
92
|
+
|
|
93
|
+
/** Account is suspended or locked */
|
|
94
|
+
data object AccountSuspended : Auth
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Unexpected / unclassified ────────────────────────────────────────
|
|
98
|
+
data class Unexpected(val cause: Throwable) : AppError
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Wrapping as Exception (for Result interop)
|
|
105
|
+
|
|
106
|
+
```kotlin
|
|
107
|
+
// ✅ Wrap AppError in an exception so it can travel through Result<T>
|
|
108
|
+
class AppException(val error: AppError) : Exception(error.toString())
|
|
109
|
+
|
|
110
|
+
// Extension to lift AppError into Result failure
|
|
111
|
+
fun <T> Result.Companion.domainFailure(error: AppError): Result<T> =
|
|
112
|
+
failure(AppException(error))
|
|
113
|
+
|
|
114
|
+
// Extension to extract AppError from Result failure
|
|
115
|
+
fun <T> Result<T>.appErrorOrNull(): AppError? =
|
|
116
|
+
exceptionOrNull()?.let { if (it is AppException) it.error else null }
|
|
117
|
+
|
|
118
|
+
// ✅ Usage in UseCase
|
|
119
|
+
class GetUserUseCase @Inject constructor(
|
|
120
|
+
private val repository: UserRepository
|
|
121
|
+
) {
|
|
122
|
+
suspend operator fun invoke(id: String): Result<User> =
|
|
123
|
+
repository.getUser(id).fold(
|
|
124
|
+
onSuccess = { user ->
|
|
125
|
+
if (user.status == UserStatus.DELETED)
|
|
126
|
+
Result.domainFailure(AppError.Data.InvalidState("User is deleted"))
|
|
127
|
+
else
|
|
128
|
+
Result.success(user)
|
|
129
|
+
},
|
|
130
|
+
onFailure = { Result.failure(it) }
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Per-Feature Error Scoping
|
|
138
|
+
|
|
139
|
+
```kotlin
|
|
140
|
+
// ✅ Feature-specific errors extend AppError — keeps domain focused
|
|
141
|
+
sealed interface OrderError : AppError {
|
|
142
|
+
data object CartEmpty : OrderError
|
|
143
|
+
data class InsufficientStock(val productName: String, val available: Int) : OrderError
|
|
144
|
+
data object PaymentDeclined : OrderError
|
|
145
|
+
data class DeliveryUnavailable(val reason: String) : OrderError
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ✅ Usage
|
|
149
|
+
class PlaceOrderUseCase @Inject constructor(
|
|
150
|
+
private val orderRepository: OrderRepository,
|
|
151
|
+
private val inventoryRepository: InventoryRepository
|
|
152
|
+
) {
|
|
153
|
+
suspend operator fun invoke(cartId: String): Result<Order> = runCatching {
|
|
154
|
+
val cart = orderRepository.getCart(cartId).getOrThrow()
|
|
155
|
+
|
|
156
|
+
if (cart.items.isEmpty())
|
|
157
|
+
throw AppException(OrderError.CartEmpty)
|
|
158
|
+
|
|
159
|
+
cart.items.forEach { item ->
|
|
160
|
+
val stock = inventoryRepository.getStock(item.productId).getOrThrow()
|
|
161
|
+
if (stock < item.quantity)
|
|
162
|
+
throw AppException(OrderError.InsufficientStock(item.name, stock))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
orderRepository.placeOrder(cart).getOrThrow()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Handling in ViewModel
|
|
173
|
+
|
|
174
|
+
```kotlin
|
|
175
|
+
// ✅ Exhaustive when expression forces handling every error
|
|
176
|
+
private fun handleError(error: AppError): String = when (error) {
|
|
177
|
+
is AppError.Network.NoConnection -> "No internet connection"
|
|
178
|
+
is AppError.Network.Timeout -> "Request timed out"
|
|
179
|
+
is AppError.Network.Unauthorized -> "Session expired"
|
|
180
|
+
is AppError.Network.Forbidden -> "Permission denied"
|
|
181
|
+
is AppError.Network.NotFound -> "Not found"
|
|
182
|
+
is AppError.Network.ServerError -> "Server error (${error.code})"
|
|
183
|
+
is AppError.Network.RateLimited -> "Too many requests"
|
|
184
|
+
is AppError.Data.NotFound -> "Item not found"
|
|
185
|
+
is AppError.Data.Conflict -> "Already exists: ${error.field}"
|
|
186
|
+
is AppError.Data.Validation -> error.errors.values.first()
|
|
187
|
+
is AppError.Data.InvalidState -> error.reason
|
|
188
|
+
is AppError.Storage.DiskFull -> "Storage full"
|
|
189
|
+
is AppError.Storage.Corrupted -> "Data corrupted"
|
|
190
|
+
is AppError.Storage.Unknown -> "Storage error"
|
|
191
|
+
is AppError.Auth.NotAuthenticated -> "Please log in"
|
|
192
|
+
is AppError.Auth.BiometricFailed -> "Authentication failed"
|
|
193
|
+
is AppError.Auth.AccountSuspended -> "Account suspended"
|
|
194
|
+
is AppError.Unexpected -> "Something went wrong"
|
|
195
|
+
// Feature errors
|
|
196
|
+
is OrderError.CartEmpty -> "Your cart is empty"
|
|
197
|
+
is OrderError.InsufficientStock -> "Not enough stock for ${error.productName}"
|
|
198
|
+
is OrderError.PaymentDeclined -> "Payment declined"
|
|
199
|
+
is OrderError.DeliveryUnavailable -> error.reason
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Retry Decision
|
|
206
|
+
|
|
207
|
+
```kotlin
|
|
208
|
+
// ✅ Determine if an error is retryable
|
|
209
|
+
fun AppError.isRetryable(): Boolean = when (this) {
|
|
210
|
+
is AppError.Network.NoConnection,
|
|
211
|
+
is AppError.Network.Timeout,
|
|
212
|
+
is AppError.Network.ServerError -> true
|
|
213
|
+
is AppError.Network.RateLimited -> true
|
|
214
|
+
else -> false
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ✅ Determine if error requires re-authentication
|
|
218
|
+
fun AppError.requiresReAuth(): Boolean =
|
|
219
|
+
this is AppError.Network.Unauthorized || this is AppError.Auth.NotAuthenticated
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Anti-Patterns
|
|
225
|
+
|
|
226
|
+
- `AppError.Unexpected` as the only error type — no granularity; can't make smart UI decisions
|
|
227
|
+
- HTTP codes in domain error names (`Http404`, `Http401`) — domain must speak business language
|
|
228
|
+
- Domain error importing `retrofit2.HttpException` — breaks domain layer independence
|
|
229
|
+
- Flat error list instead of sealed hierarchy — no compile-time exhaustiveness
|
|
230
|
+
- Too many error subtypes nobody handles differently — YAGNI; add when UI needs to differentiate
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Related Skills
|
|
235
|
+
- `error-handling` — propagation strategy across layers
|
|
236
|
+
- `error-mapping` — converting raw exceptions to domain errors
|
|
237
|
+
- `failure-strategy` — deciding how to respond to each error
|
|
238
|
+
- `user-friendly-errors` — displaying errors in UI
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: error-handling
|
|
3
|
+
description: >
|
|
4
|
+
Error handling strategy for Android Clean Architecture projects.
|
|
5
|
+
Load this skill when designing how errors propagate across layers,
|
|
6
|
+
using Result<T> for error representation, handling errors in ViewModel,
|
|
7
|
+
or deciding between exceptions and sealed classes for error modeling.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Error Handling
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Error handling defines how failures travel from the data layer to the UI. In Clean Architecture, errors are represented as `Result<T>` in the domain and data layers, mapped to domain-specific error types, and finally translated to user-friendly messages in the presentation layer. Exceptions are caught at layer boundaries — never leaked into the UI.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- **Catch exceptions at boundaries** — Data layer catches, wraps in `Result.failure()`
|
|
20
|
+
- **Domain errors are sealed classes** — not raw exceptions; typed and exhaustive
|
|
21
|
+
- **ViewModel translates** domain errors to UI state — never expose raw exceptions to UI
|
|
22
|
+
- **Never swallow errors silently** — always log or propagate
|
|
23
|
+
- **`runCatching`** for converting exception-throwing code to `Result<T>`
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Result<T> Across Layers
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Data Layer Domain Layer Presentation Layer
|
|
31
|
+
───────────────── ────────────────── ───────────────────
|
|
32
|
+
Repository UseCase ViewModel
|
|
33
|
+
runCatching { → Result<T> → UiState.Error(msg)
|
|
34
|
+
api.call() → .map { } →
|
|
35
|
+
} → .mapError { } →
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Data Layer — Catching at the Boundary
|
|
41
|
+
|
|
42
|
+
```kotlin
|
|
43
|
+
// ✅ Repository wraps all exceptions in Result
|
|
44
|
+
class UserRepositoryImpl @Inject constructor(
|
|
45
|
+
private val api: UserApiService,
|
|
46
|
+
private val dao: UserDao
|
|
47
|
+
) : UserRepository {
|
|
48
|
+
|
|
49
|
+
override suspend fun getUser(id: String): Result<User> = runCatching {
|
|
50
|
+
dao.getById(id)?.toDomain()
|
|
51
|
+
?: api.getUser(id).toDomain().also { dao.insert(it.toEntity()) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override suspend fun createUser(user: User): Result<User> = runCatching {
|
|
55
|
+
val dto = api.createUser(user.toCreateRequest())
|
|
56
|
+
val created = dto.toDomain()
|
|
57
|
+
dao.insert(created.toEntity())
|
|
58
|
+
created
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ✅ Flow — use catch operator
|
|
62
|
+
override fun observeUsers(): Flow<List<User>> =
|
|
63
|
+
dao.observeAll()
|
|
64
|
+
.map { entities -> entities.map { it.toDomain() } }
|
|
65
|
+
.catch { e ->
|
|
66
|
+
// Log and emit empty — or rethrow depending on strategy
|
|
67
|
+
Timber.e(e, "Failed to observe users")
|
|
68
|
+
emit(emptyList())
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Domain Layer — Typed Error Model
|
|
76
|
+
|
|
77
|
+
```kotlin
|
|
78
|
+
// ✅ Domain errors — see domain-error-model skill for full detail
|
|
79
|
+
sealed interface UserError {
|
|
80
|
+
data object NotFound : UserError
|
|
81
|
+
data object Unauthorized : UserError
|
|
82
|
+
data class NetworkError(val message: String) : UserError
|
|
83
|
+
data class ValidationError(val field: String, val reason: String) : UserError
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ✅ UseCase maps raw Result to domain Result
|
|
87
|
+
class GetUserUseCase @Inject constructor(
|
|
88
|
+
private val repository: UserRepository
|
|
89
|
+
) {
|
|
90
|
+
suspend operator fun invoke(id: String): Result<User> =
|
|
91
|
+
repository.getUser(id)
|
|
92
|
+
.mapCatching { user ->
|
|
93
|
+
// Additional domain validation
|
|
94
|
+
check(user.status != UserStatus.DELETED) { "User has been deleted" }
|
|
95
|
+
user
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Presentation Layer — ViewModel Error Handling
|
|
103
|
+
|
|
104
|
+
```kotlin
|
|
105
|
+
// ✅ ViewModel translates Result to UiState
|
|
106
|
+
@HiltViewModel
|
|
107
|
+
class UserDetailViewModel @Inject constructor(
|
|
108
|
+
savedStateHandle: SavedStateHandle,
|
|
109
|
+
private val getUserUseCase: GetUserUseCase,
|
|
110
|
+
private val deleteUserUseCase: DeleteUserUseCase
|
|
111
|
+
) : ViewModel() {
|
|
112
|
+
|
|
113
|
+
private val userId = checkNotNull(savedStateHandle.get<String>("userId"))
|
|
114
|
+
|
|
115
|
+
private val _state = MutableStateFlow<UserDetailUiState>(UserDetailUiState.Loading)
|
|
116
|
+
val state: StateFlow<UserDetailUiState> = _state.asStateFlow()
|
|
117
|
+
|
|
118
|
+
private val _events = Channel<UserDetailEvent>(Channel.BUFFERED)
|
|
119
|
+
val events: Flow<UserDetailEvent> = _events.receiveAsFlow()
|
|
120
|
+
|
|
121
|
+
init { loadUser() }
|
|
122
|
+
|
|
123
|
+
fun loadUser() {
|
|
124
|
+
viewModelScope.launch {
|
|
125
|
+
_state.value = UserDetailUiState.Loading
|
|
126
|
+
getUserUseCase(userId).fold(
|
|
127
|
+
onSuccess = { user ->
|
|
128
|
+
_state.value = UserDetailUiState.Success(user)
|
|
129
|
+
},
|
|
130
|
+
onFailure = { error ->
|
|
131
|
+
_state.value = UserDetailUiState.Error(
|
|
132
|
+
message = error.toUserMessage(),
|
|
133
|
+
canRetry = error.isRetryable()
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fun onDeleteClick() {
|
|
141
|
+
viewModelScope.launch {
|
|
142
|
+
deleteUserUseCase(userId).fold(
|
|
143
|
+
onSuccess = {
|
|
144
|
+
_events.send(UserDetailEvent.NavigateBack)
|
|
145
|
+
},
|
|
146
|
+
onFailure = { error ->
|
|
147
|
+
_events.send(UserDetailEvent.ShowSnackbar(error.toUserMessage()))
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Error Extension Functions
|
|
158
|
+
|
|
159
|
+
```kotlin
|
|
160
|
+
// ✅ Centralized error translation
|
|
161
|
+
fun Throwable.toUserMessage(): String = when (this) {
|
|
162
|
+
is HttpException -> when (code()) {
|
|
163
|
+
401 -> "Session expired. Please log in again."
|
|
164
|
+
403 -> "You don't have permission to do this."
|
|
165
|
+
404 -> "The requested item was not found."
|
|
166
|
+
429 -> "Too many requests. Please wait a moment."
|
|
167
|
+
in 500..599 -> "Server error. Please try again later."
|
|
168
|
+
else -> "Something went wrong. Please try again."
|
|
169
|
+
}
|
|
170
|
+
is IOException,
|
|
171
|
+
is SocketTimeoutException -> "No internet connection. Please check your network."
|
|
172
|
+
is CancellationException -> throw this // never swallow cancellation
|
|
173
|
+
else -> message ?: "An unexpected error occurred."
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun Throwable.isRetryable(): Boolean = when (this) {
|
|
177
|
+
is IOException,
|
|
178
|
+
is SocketTimeoutException -> true
|
|
179
|
+
is HttpException -> code() in 500..599
|
|
180
|
+
else -> false
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ✅ Result extensions
|
|
184
|
+
fun <T> Result<T>.onFailureLog(tag: String = "App"): Result<T> = onFailure { e ->
|
|
185
|
+
if (e !is CancellationException) Timber.tag(tag).e(e)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
suspend fun <T> Result<T>.orThrowDomain(
|
|
189
|
+
transform: (Throwable) -> Throwable = { it }
|
|
190
|
+
): T = fold(
|
|
191
|
+
onSuccess = { it },
|
|
192
|
+
onFailure = { throw transform(it) }
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Flow Error Handling
|
|
199
|
+
|
|
200
|
+
```kotlin
|
|
201
|
+
// ✅ Wrap Flow emissions in Result for error propagation
|
|
202
|
+
fun <T> Flow<T>.asResult(): Flow<Result<T>> =
|
|
203
|
+
map { Result.success(it) }
|
|
204
|
+
.catch { emit(Result.failure(it)) }
|
|
205
|
+
|
|
206
|
+
// ✅ Usage in ViewModel
|
|
207
|
+
val state: StateFlow<UserListUiState> =
|
|
208
|
+
getUsersUseCase()
|
|
209
|
+
.asResult()
|
|
210
|
+
.map { result ->
|
|
211
|
+
result.fold(
|
|
212
|
+
onSuccess = { UserListUiState.Success(it) },
|
|
213
|
+
onFailure = { UserListUiState.Error(it.toUserMessage()) }
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UserListUiState.Loading)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Never Swallow CancellationException
|
|
222
|
+
|
|
223
|
+
```kotlin
|
|
224
|
+
// ✅ Always rethrow CancellationException
|
|
225
|
+
try {
|
|
226
|
+
someOperation()
|
|
227
|
+
} catch (e: CancellationException) {
|
|
228
|
+
throw e // ✅ must rethrow — coroutine cancellation mechanism depends on this
|
|
229
|
+
} catch (e: Exception) {
|
|
230
|
+
handleError(e)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ✅ runCatching is safe — it doesn't swallow CancellationException in Kotlin 1.7+
|
|
234
|
+
val result = runCatching { someOperation() }
|
|
235
|
+
// But be careful with older Kotlin versions
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Anti-Patterns
|
|
241
|
+
|
|
242
|
+
- Catching `Exception` in ViewModel and showing generic message — typed errors give better UX
|
|
243
|
+
- Leaking `HttpException` or `IOException` into Domain layer — map at the repository boundary
|
|
244
|
+
- Empty `catch` blocks — always log or propagate
|
|
245
|
+
- Swallowing `CancellationException` — breaks coroutine cancellation
|
|
246
|
+
- Storing error state as `String?` in UiState — use a sealed class for typed error display
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Related Skills
|
|
251
|
+
- `domain-error-model` — typed domain error hierarchy
|
|
252
|
+
- `error-mapping` — mapping between layer-specific errors
|
|
253
|
+
- `failure-strategy` — deciding how to respond to different error types
|
|
254
|
+
- `user-friendly-errors` — translating technical errors to UI messages
|
|
255
|
+
- `state-management` — modeling error state in UiState
|