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,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: use-case-design
|
|
3
|
+
description: >
|
|
4
|
+
Use Case design for Android Clean Architecture.
|
|
5
|
+
Load this skill when creating UseCase classes, deciding what logic
|
|
6
|
+
belongs in a UseCase vs Repository vs ViewModel, structuring UseCase
|
|
7
|
+
inputs and outputs, or composing multiple UseCases together.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Use Case Design
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
A UseCase (also called Interactor) encapsulates a single business operation. It lives in the Domain layer, depends only on Repository interfaces and domain models, and is called by the ViewModel. ViewModels never call Repositories directly — they always go through a UseCase.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- **One UseCase, one operation** — `GetUserUseCase`, not `UserUseCase`
|
|
21
|
+
- Lives in **Domain layer** — pure Kotlin, no Android imports
|
|
22
|
+
- Depends on **Repository interfaces** — not implementations
|
|
23
|
+
- Called by **ViewModel only** — never from UI or another UseCase (compose at ViewModel level)
|
|
24
|
+
- Returns **Result<T>** for suspend operations, **Flow<T>** for reactive streams
|
|
25
|
+
- **No state** — UseCases are stateless; state lives in ViewModel
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Standard UseCase (Suspend)
|
|
30
|
+
|
|
31
|
+
```kotlin
|
|
32
|
+
// ✅ Suspend UseCase — one operation, invokable via operator fun
|
|
33
|
+
class GetUserUseCase @Inject constructor(
|
|
34
|
+
private val userRepository: UserRepository
|
|
35
|
+
) {
|
|
36
|
+
suspend operator fun invoke(userId: String): Result<User> =
|
|
37
|
+
userRepository.getUser(userId)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class DeleteUserUseCase @Inject constructor(
|
|
41
|
+
private val userRepository: UserRepository
|
|
42
|
+
) {
|
|
43
|
+
suspend operator fun invoke(userId: String): Result<Unit> =
|
|
44
|
+
userRepository.deleteUser(userId)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class CreateOrderUseCase @Inject constructor(
|
|
48
|
+
private val orderRepository: OrderRepository,
|
|
49
|
+
private val cartRepository: CartRepository,
|
|
50
|
+
private val inventoryRepository: InventoryRepository
|
|
51
|
+
) {
|
|
52
|
+
suspend operator fun invoke(cartId: String, userId: String): Result<Order> =
|
|
53
|
+
runCatching {
|
|
54
|
+
val cart = cartRepository.getCart(cartId).getOrThrow()
|
|
55
|
+
require(cart.items.isNotEmpty()) { "Cannot create order from empty cart" }
|
|
56
|
+
|
|
57
|
+
// Business rule: validate inventory before placing order
|
|
58
|
+
cart.items.forEach { item ->
|
|
59
|
+
val available = inventoryRepository.getStock(item.productId).getOrThrow()
|
|
60
|
+
require(available >= item.quantity) {
|
|
61
|
+
"Insufficient stock for ${item.productName}"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
val order = orderRepository.create(cart.toOrderRequest(userId)).getOrThrow()
|
|
66
|
+
cartRepository.clear(cartId)
|
|
67
|
+
order
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Reactive UseCase (Flow)
|
|
75
|
+
|
|
76
|
+
```kotlin
|
|
77
|
+
// ✅ Flow UseCase — for ongoing observation
|
|
78
|
+
class ObserveUsersUseCase @Inject constructor(
|
|
79
|
+
private val userRepository: UserRepository
|
|
80
|
+
) {
|
|
81
|
+
operator fun invoke(): Flow<List<User>> =
|
|
82
|
+
userRepository.observeUsers()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ✅ Flow UseCase with transformation
|
|
86
|
+
class ObserveActiveUsersUseCase @Inject constructor(
|
|
87
|
+
private val userRepository: UserRepository
|
|
88
|
+
) {
|
|
89
|
+
operator fun invoke(): Flow<List<User>> =
|
|
90
|
+
userRepository.observeUsers()
|
|
91
|
+
.map { users -> users.filter { it.status == UserStatus.ACTIVE } }
|
|
92
|
+
.distinctUntilChanged()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ✅ Flow UseCase combining multiple sources
|
|
96
|
+
class ObserveDashboardUseCase @Inject constructor(
|
|
97
|
+
private val userRepository: UserRepository,
|
|
98
|
+
private val orderRepository: OrderRepository
|
|
99
|
+
) {
|
|
100
|
+
operator fun invoke(userId: String): Flow<DashboardData> =
|
|
101
|
+
combine(
|
|
102
|
+
userRepository.observeUser(userId),
|
|
103
|
+
orderRepository.observeRecentOrders(userId)
|
|
104
|
+
) { user, orders ->
|
|
105
|
+
DashboardData(
|
|
106
|
+
userName = user?.name ?: "",
|
|
107
|
+
recentOrderCount = orders.size,
|
|
108
|
+
pendingOrderCount = orders.count { it.status == OrderStatus.PENDING }
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## UseCase with Parameters
|
|
117
|
+
|
|
118
|
+
```kotlin
|
|
119
|
+
// ✅ Simple primitive params — pass directly
|
|
120
|
+
class SearchProductsUseCase @Inject constructor(
|
|
121
|
+
private val repository: ProductRepository
|
|
122
|
+
) {
|
|
123
|
+
suspend operator fun invoke(query: String, filter: ProductFilter): Result<List<Product>> =
|
|
124
|
+
repository.search(query, filter)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ✅ Complex params — use a dedicated Params data class
|
|
128
|
+
class PlaceOrderUseCase @Inject constructor(
|
|
129
|
+
private val repository: OrderRepository
|
|
130
|
+
) {
|
|
131
|
+
data class Params(
|
|
132
|
+
val cartId: String,
|
|
133
|
+
val shippingAddressId: String,
|
|
134
|
+
val paymentMethodId: String,
|
|
135
|
+
val couponCode: String? = null
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
suspend operator fun invoke(params: Params): Result<Order> =
|
|
139
|
+
runCatching {
|
|
140
|
+
// apply coupon if present
|
|
141
|
+
val discount = params.couponCode?.let {
|
|
142
|
+
repository.validateCoupon(it).getOrThrow()
|
|
143
|
+
}
|
|
144
|
+
repository.placeOrder(params.toRequest(discount)).getOrThrow()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ViewModel usage
|
|
149
|
+
fun onPlaceOrder() {
|
|
150
|
+
viewModelScope.launch {
|
|
151
|
+
val params = PlaceOrderUseCase.Params(
|
|
152
|
+
cartId = state.value.cartId,
|
|
153
|
+
shippingAddressId = state.value.selectedAddressId,
|
|
154
|
+
paymentMethodId = state.value.selectedPaymentId,
|
|
155
|
+
couponCode = state.value.couponCode.takeIf { it.isNotBlank() }
|
|
156
|
+
)
|
|
157
|
+
placeOrderUseCase(params).fold(
|
|
158
|
+
onSuccess = { _events.send(Event.NavigateToConfirmation(it.id)) },
|
|
159
|
+
onFailure = { _state.update { s -> s.copy(error = it.message) } }
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## ViewModel Usage
|
|
168
|
+
|
|
169
|
+
```kotlin
|
|
170
|
+
// ✅ ViewModel composes UseCases — never calls Repository directly
|
|
171
|
+
@HiltViewModel
|
|
172
|
+
class UserDetailViewModel @Inject constructor(
|
|
173
|
+
savedStateHandle: SavedStateHandle,
|
|
174
|
+
private val getUserUseCase: GetUserUseCase,
|
|
175
|
+
private val updateUserUseCase: UpdateUserUseCase,
|
|
176
|
+
private val deleteUserUseCase: DeleteUserUseCase
|
|
177
|
+
) : ViewModel() {
|
|
178
|
+
|
|
179
|
+
private val userId: String = checkNotNull(savedStateHandle["userId"])
|
|
180
|
+
|
|
181
|
+
val state: StateFlow<UserDetailUiState> =
|
|
182
|
+
getUserUseCase.asFlow(userId) // extension if needed
|
|
183
|
+
.map { result ->
|
|
184
|
+
result.fold(
|
|
185
|
+
onSuccess = { UserDetailUiState.Success(it) },
|
|
186
|
+
onFailure = { UserDetailUiState.Error(it.message ?: "Error") }
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UserDetailUiState.Loading)
|
|
190
|
+
|
|
191
|
+
fun onDeleteClick() {
|
|
192
|
+
viewModelScope.launch {
|
|
193
|
+
deleteUserUseCase(userId).fold(
|
|
194
|
+
onSuccess = { _events.send(Event.NavigateBack) },
|
|
195
|
+
onFailure = { _events.send(Event.ShowError(it.message ?: "Delete failed")) }
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## UseCase Package Structure
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
domain/
|
|
208
|
+
└── usecase/
|
|
209
|
+
├── user/
|
|
210
|
+
│ ├── GetUserUseCase.kt
|
|
211
|
+
│ ├── GetUsersUseCase.kt
|
|
212
|
+
│ ├── ObserveUsersUseCase.kt
|
|
213
|
+
│ ├── CreateUserUseCase.kt
|
|
214
|
+
│ ├── UpdateUserUseCase.kt
|
|
215
|
+
│ └── DeleteUserUseCase.kt
|
|
216
|
+
├── order/
|
|
217
|
+
│ ├── PlaceOrderUseCase.kt
|
|
218
|
+
│ ├── CancelOrderUseCase.kt
|
|
219
|
+
│ └── ObserveOrdersUseCase.kt
|
|
220
|
+
└── auth/
|
|
221
|
+
├── LoginUseCase.kt
|
|
222
|
+
└── LogoutUseCase.kt
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Anti-Patterns
|
|
228
|
+
|
|
229
|
+
- ViewModel calling Repository directly — always go through UseCase
|
|
230
|
+
- UseCase with multiple unrelated responsibilities — split into separate UseCases
|
|
231
|
+
- UseCase holding state — stateless only; state belongs in ViewModel
|
|
232
|
+
- UseCase importing Android framework classes — Domain layer is pure Kotlin
|
|
233
|
+
- Generic `UserUseCase` with many methods — one class, one operation
|
|
234
|
+
- UseCase calling another UseCase — compose at ViewModel level instead
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Related Skills
|
|
239
|
+
|
|
240
|
+
- `clean-architecture` — layer structure UseCase sits within
|
|
241
|
+
- `mvvm` — ViewModel calling UseCases
|
|
242
|
+
- `repository-pattern` — Repository interfaces UseCase depends on
|
|
243
|
+
- `domain-modeling` — domain models UseCase operates on
|
|
244
|
+
- `side-effect-management` — handling side effects triggered by UseCases
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: value-object
|
|
3
|
+
description: >
|
|
4
|
+
Value Object pattern for Android domain modeling.
|
|
5
|
+
Load this skill when wrapping primitive types with business meaning,
|
|
6
|
+
implementing validation at construction time, designing immutable value types,
|
|
7
|
+
using Kotlin inline/value classes, or eliminating primitive obsession.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Value Object
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
A Value Object is an immutable object defined entirely by its values, not by identity. Two Value Objects with the same values are equal. They encapsulate validation and domain rules at the point of creation, eliminating invalid states and primitive obsession across the codebase.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- **No identity** — equality is based on values, not `id`
|
|
21
|
+
- **Immutable** — all fields are `val`; produce new instances instead of mutating
|
|
22
|
+
- **Self-validating** — invalid values cannot be constructed; validation in `init`
|
|
23
|
+
- **Behavior-rich** — operations and rules belong here, not in the caller
|
|
24
|
+
- **Replaces primitives** — prefer `Email` over `String`, `Money` over `Long`
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Kotlin Value Class (Inline Class)
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ Single-field value object — use @JvmInline value class (zero overhead)
|
|
32
|
+
@JvmInline
|
|
33
|
+
value class Email(val value: String) {
|
|
34
|
+
init {
|
|
35
|
+
require(value.isNotBlank()) { "Email cannot be blank" }
|
|
36
|
+
require(value.contains("@")) { "Invalid email format: $value" }
|
|
37
|
+
require(value.length <= 254) { "Email exceeds maximum length" }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
val domain: String get() = value.substringAfter("@")
|
|
41
|
+
val local: String get() = value.substringBefore("@")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@JvmInline
|
|
45
|
+
value class UserId(val value: String) {
|
|
46
|
+
init { require(value.isNotBlank()) { "UserId cannot be blank" } }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@JvmInline
|
|
50
|
+
value class PhoneNumber(val value: String) {
|
|
51
|
+
init {
|
|
52
|
+
val digits = value.filter { it.isDigit() }
|
|
53
|
+
require(digits.length in 10..15) { "Invalid phone number: $value" }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
val normalized: String get() = value.filter { it.isDigit() }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@JvmInline
|
|
60
|
+
value class Percentage(val value: Double) {
|
|
61
|
+
init {
|
|
62
|
+
require(value in 0.0..100.0) { "Percentage must be between 0 and 100: $value" }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fun toFraction(): Double = value / 100.0
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Multi-Field Value Object
|
|
72
|
+
|
|
73
|
+
```kotlin
|
|
74
|
+
// ✅ Multi-field value object — regular data class
|
|
75
|
+
data class Money(
|
|
76
|
+
val amount: Long, // smallest unit (e.g. cents, rials)
|
|
77
|
+
val currency: Currency
|
|
78
|
+
) {
|
|
79
|
+
init {
|
|
80
|
+
require(amount >= 0) { "Money amount cannot be negative: $amount" }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
operator fun plus(other: Money): Money {
|
|
84
|
+
require(currency == other.currency) { "Cannot add different currencies" }
|
|
85
|
+
return copy(amount = amount + other.amount)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
operator fun minus(other: Money): Money {
|
|
89
|
+
require(currency == other.currency) { "Cannot subtract different currencies" }
|
|
90
|
+
require(amount >= other.amount) { "Cannot subtract: result would be negative" }
|
|
91
|
+
return copy(amount = amount - other.amount)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
operator fun times(factor: Int): Money = copy(amount = amount * factor)
|
|
95
|
+
|
|
96
|
+
fun isZero(): Boolean = amount == 0L
|
|
97
|
+
fun isPositive(): Boolean = amount > 0L
|
|
98
|
+
|
|
99
|
+
companion object {
|
|
100
|
+
fun zero(currency: Currency) = Money(0L, currency)
|
|
101
|
+
fun ofRials(amount: Long) = Money(amount, Currency.IRR)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
data class Address(
|
|
106
|
+
val street: String,
|
|
107
|
+
val city: String,
|
|
108
|
+
val postalCode: PostalCode,
|
|
109
|
+
val country: Country
|
|
110
|
+
) {
|
|
111
|
+
init {
|
|
112
|
+
require(street.isNotBlank()) { "Street cannot be blank" }
|
|
113
|
+
require(city.isNotBlank()) { "City cannot be blank" }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fun formatted(): String = "$street, $city, ${postalCode.value}, ${country.displayName}"
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
data class DateRange(
|
|
120
|
+
val start: LocalDate,
|
|
121
|
+
val end: LocalDate
|
|
122
|
+
) {
|
|
123
|
+
init {
|
|
124
|
+
require(!end.isBefore(start)) { "End date must not be before start date" }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
fun contains(date: LocalDate): Boolean =
|
|
128
|
+
!date.isBefore(start) && !date.isAfter(end)
|
|
129
|
+
|
|
130
|
+
fun overlaps(other: DateRange): Boolean =
|
|
131
|
+
!start.isAfter(other.end) && !end.isBefore(other.start)
|
|
132
|
+
|
|
133
|
+
fun durationDays(): Long = ChronoUnit.DAYS.between(start, end)
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Safe Construction with Result
|
|
140
|
+
|
|
141
|
+
```kotlin
|
|
142
|
+
// ✅ When validation failure should be handled gracefully (not crash)
|
|
143
|
+
@JvmInline
|
|
144
|
+
value class Username private constructor(val value: String) {
|
|
145
|
+
companion object {
|
|
146
|
+
fun create(value: String): Result<Username> = runCatching {
|
|
147
|
+
require(value.length in 3..20) { "Username must be 3-20 characters" }
|
|
148
|
+
require(value.all { it.isLetterOrDigit() || it == '_' }) {
|
|
149
|
+
"Username can only contain letters, digits, and underscores"
|
|
150
|
+
}
|
|
151
|
+
Username(value)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Usage
|
|
157
|
+
val result = Username.create(input)
|
|
158
|
+
result.fold(
|
|
159
|
+
onSuccess = { username -> proceed(username) },
|
|
160
|
+
onFailure = { error -> showValidationError(error.message) }
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Storage Mapping
|
|
167
|
+
|
|
168
|
+
```kotlin
|
|
169
|
+
// ✅ TypeConverter for Value Objects in Room
|
|
170
|
+
class ValueObjectConverters {
|
|
171
|
+
|
|
172
|
+
@TypeConverter
|
|
173
|
+
fun fromEmail(email: Email?): String? = email?.value
|
|
174
|
+
|
|
175
|
+
@TypeConverter
|
|
176
|
+
fun toEmail(value: String?): Email? = value?.let { Email(it) }
|
|
177
|
+
|
|
178
|
+
@TypeConverter
|
|
179
|
+
fun fromMoney(money: Money?): String? =
|
|
180
|
+
money?.let { "${it.amount}:${it.currency.name}" }
|
|
181
|
+
|
|
182
|
+
@TypeConverter
|
|
183
|
+
fun toMoney(value: String?): Money? = value?.let {
|
|
184
|
+
val (amount, currency) = it.split(":")
|
|
185
|
+
Money(amount.toLong(), Currency.valueOf(currency))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Serialization Mapping
|
|
193
|
+
|
|
194
|
+
```kotlin
|
|
195
|
+
// ✅ Never serialize Value Objects directly — map at the boundary
|
|
196
|
+
data class UserDto(
|
|
197
|
+
@SerialName("id") val id: String,
|
|
198
|
+
@SerialName("email") val email: String, // raw String in DTO
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// ✅ Map to domain Value Object in mapper
|
|
202
|
+
fun UserDto.toDomain(): User = User(
|
|
203
|
+
id = UserId(id),
|
|
204
|
+
email = Email(email), // construct Value Object here
|
|
205
|
+
...
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Anti-Patterns
|
|
212
|
+
|
|
213
|
+
- Passing raw `String` for email, phone, ID across the codebase — use Value Objects
|
|
214
|
+
- Validating the same constraint in multiple places — validation belongs in the Value Object constructor
|
|
215
|
+
- Mutable Value Objects (`var` fields) — Value Objects must be immutable
|
|
216
|
+
- Using `@JvmInline value class` for multi-field objects — value classes only support one property
|
|
217
|
+
- Throwing generic `IllegalArgumentException` without a message — always include context in the message
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Related Skills
|
|
222
|
+
|
|
223
|
+
- `domain-modeling` — where Value Objects are used within the domain
|
|
224
|
+
- `entity-design` — mapping Value Objects to database columns
|
|
225
|
+
- `dto-mapping` — mapping between raw DTO strings and Value Objects
|
|
226
|
+
- `immutability` — immutability principles in Kotlin
|