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,236 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: domain-modeling
|
|
3
|
+
description: >
|
|
4
|
+
Domain modeling for Android Clean Architecture projects.
|
|
5
|
+
Load this skill when designing domain entities, defining business rules
|
|
6
|
+
in the domain layer, modeling relationships between domain objects,
|
|
7
|
+
choosing between Entity vs Value Object, or structuring the domain package.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Domain Modeling
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Domain modeling defines the core business concepts of the application as pure Kotlin classes — free of Android framework, database annotations, or network serialization. The domain model is the language of the business, shared across all layers via mapping.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Domain models are **pure Kotlin** — no `@Entity`, `@SerialName`, `@Parcelize`
|
|
21
|
+
- Domain models express **business concepts** — not database tables or API contracts
|
|
22
|
+
- **Behavior belongs in domain** — validation, business rules, computations live here
|
|
23
|
+
- **Immutable** — use `val`, use `data class`, use `copy()` for changes
|
|
24
|
+
- Domain models are the **shared language** — same names used in UI, DB, API (via mapping)
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Entity vs Value Object
|
|
29
|
+
|
|
30
|
+
| | Entity | Value Object |
|
|
31
|
+
| --------- | ------------------------ | -------------------------- |
|
|
32
|
+
| Identity | Has unique `id` | Defined by its values |
|
|
33
|
+
| Equality | By `id` | By all fields |
|
|
34
|
+
| Lifecycle | Persisted, tracked | Transient, replaceable |
|
|
35
|
+
| Example | `User(id=1, name="Ali")` | `Email("ali@example.com")` |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Domain Entity
|
|
40
|
+
|
|
41
|
+
```kotlin
|
|
42
|
+
// ✅ Entity — has identity, represents a business object
|
|
43
|
+
data class User(
|
|
44
|
+
val id: String,
|
|
45
|
+
val name: String,
|
|
46
|
+
val email: Email, // ✅ use Value Object for validated fields
|
|
47
|
+
val role: UserRole,
|
|
48
|
+
val status: UserStatus,
|
|
49
|
+
val createdAt: Instant
|
|
50
|
+
) {
|
|
51
|
+
// ✅ Business rules as functions — not in ViewModel
|
|
52
|
+
fun canEdit(actor: User): Boolean =
|
|
53
|
+
actor.id == id || actor.role == UserRole.ADMIN
|
|
54
|
+
|
|
55
|
+
fun isActive(): Boolean = status == UserStatus.ACTIVE
|
|
56
|
+
|
|
57
|
+
fun withRole(newRole: UserRole): User = copy(role = newRole)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
data class Order(
|
|
61
|
+
val id: String,
|
|
62
|
+
val customerId: String,
|
|
63
|
+
val items: List<OrderItem>,
|
|
64
|
+
val status: OrderStatus,
|
|
65
|
+
val placedAt: Instant
|
|
66
|
+
) {
|
|
67
|
+
val totalAmount: Money
|
|
68
|
+
get() = items.fold(Money.ZERO) { acc, item -> acc + item.subtotal }
|
|
69
|
+
|
|
70
|
+
fun canBeCancelled(): Boolean =
|
|
71
|
+
status == OrderStatus.PENDING || status == OrderStatus.PROCESSING
|
|
72
|
+
|
|
73
|
+
fun withStatus(newStatus: OrderStatus): Order = copy(status = newStatus)
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Value Object
|
|
80
|
+
|
|
81
|
+
```kotlin
|
|
82
|
+
// ✅ Value Object — validated, no identity
|
|
83
|
+
@JvmInline
|
|
84
|
+
value class Email(val value: String) {
|
|
85
|
+
init {
|
|
86
|
+
require(value.contains("@")) { "Invalid email: $value" }
|
|
87
|
+
require(value.length <= 254) { "Email too long" }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@JvmInline
|
|
92
|
+
value class UserId(val value: String) {
|
|
93
|
+
init { require(value.isNotBlank()) { "UserId cannot be blank" } }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
data class Money(
|
|
97
|
+
val amount: Long, // stored in smallest unit (cents/rials)
|
|
98
|
+
val currency: Currency
|
|
99
|
+
) {
|
|
100
|
+
operator fun plus(other: Money): Money {
|
|
101
|
+
require(currency == other.currency) { "Currency mismatch" }
|
|
102
|
+
return copy(amount = amount + other.amount)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fun isPositive(): Boolean = amount > 0
|
|
106
|
+
|
|
107
|
+
companion object {
|
|
108
|
+
val ZERO = Money(0, Currency.IRR)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
data class DateRange(
|
|
113
|
+
val start: LocalDate,
|
|
114
|
+
val end: LocalDate
|
|
115
|
+
) {
|
|
116
|
+
init { require(!end.isBefore(start)) { "End must be after start" } }
|
|
117
|
+
|
|
118
|
+
fun contains(date: LocalDate): Boolean = !date.isBefore(start) && !date.isAfter(end)
|
|
119
|
+
fun durationDays(): Long = ChronoUnit.DAYS.between(start, end)
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Enums and Sealed Classes
|
|
126
|
+
|
|
127
|
+
```kotlin
|
|
128
|
+
// ✅ Simple states — enum
|
|
129
|
+
enum class UserRole { ADMIN, MEMBER, GUEST }
|
|
130
|
+
enum class UserStatus { ACTIVE, SUSPENDED, DELETED }
|
|
131
|
+
enum class OrderStatus { PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED }
|
|
132
|
+
|
|
133
|
+
// ✅ States with data — sealed class
|
|
134
|
+
sealed interface PaymentResult {
|
|
135
|
+
data class Success(val transactionId: String, val amount: Money) : PaymentResult
|
|
136
|
+
data class Failed(val reason: String, val code: Int) : PaymentResult
|
|
137
|
+
data object Cancelled : PaymentResult
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
sealed interface ValidationResult {
|
|
141
|
+
data object Valid : ValidationResult
|
|
142
|
+
data class Invalid(val errors: List<String>) : ValidationResult
|
|
143
|
+
|
|
144
|
+
fun isValid(): Boolean = this is Valid
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Aggregate Design
|
|
151
|
+
|
|
152
|
+
```kotlin
|
|
153
|
+
// ✅ Aggregate root — owns its children, enforces consistency
|
|
154
|
+
data class Cart(
|
|
155
|
+
val id: String,
|
|
156
|
+
val customerId: String,
|
|
157
|
+
val items: List<CartItem> = emptyList()
|
|
158
|
+
) {
|
|
159
|
+
val totalAmount: Money
|
|
160
|
+
get() = items.fold(Money.ZERO) { acc, item -> acc + item.subtotal }
|
|
161
|
+
|
|
162
|
+
val itemCount: Int get() = items.sumOf { it.quantity }
|
|
163
|
+
|
|
164
|
+
fun addItem(product: Product, quantity: Int): Cart {
|
|
165
|
+
require(quantity > 0) { "Quantity must be positive" }
|
|
166
|
+
val existing = items.find { it.productId == product.id }
|
|
167
|
+
return if (existing != null) {
|
|
168
|
+
copy(items = items.map {
|
|
169
|
+
if (it.productId == product.id)
|
|
170
|
+
it.copy(quantity = it.quantity + quantity)
|
|
171
|
+
else it
|
|
172
|
+
})
|
|
173
|
+
} else {
|
|
174
|
+
copy(items = items + CartItem(product.id, product.name, product.price, quantity))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fun removeItem(productId: String): Cart =
|
|
179
|
+
copy(items = items.filter { it.productId != productId })
|
|
180
|
+
|
|
181
|
+
fun clear(): Cart = copy(items = emptyList())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
data class CartItem(
|
|
185
|
+
val productId: String,
|
|
186
|
+
val productName: String,
|
|
187
|
+
val unitPrice: Money,
|
|
188
|
+
val quantity: Int
|
|
189
|
+
) {
|
|
190
|
+
val subtotal: Money get() = Money(unitPrice.amount * quantity, unitPrice.currency)
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Domain Package Structure
|
|
197
|
+
|
|
198
|
+
```
|
|
199
|
+
domain/
|
|
200
|
+
├── model/
|
|
201
|
+
│ ├── User.kt
|
|
202
|
+
│ ├── Order.kt
|
|
203
|
+
│ ├── Cart.kt
|
|
204
|
+
│ └── Product.kt
|
|
205
|
+
├── value/
|
|
206
|
+
│ ├── Email.kt
|
|
207
|
+
│ ├── Money.kt
|
|
208
|
+
│ ├── DateRange.kt
|
|
209
|
+
│ └── UserId.kt
|
|
210
|
+
├── repository/
|
|
211
|
+
│ ├── UserRepository.kt
|
|
212
|
+
│ └── OrderRepository.kt
|
|
213
|
+
└── usecase/
|
|
214
|
+
├── GetUserUseCase.kt
|
|
215
|
+
└── PlaceOrderUseCase.kt
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Anti-Patterns
|
|
221
|
+
|
|
222
|
+
- Domain models with `@Entity` or `@SerialName` — domain must not know about persistence or network
|
|
223
|
+
- Anemic domain model — models with only `data class` fields and no behavior; business rules scattered in ViewModel
|
|
224
|
+
- Using `String` for everything — wrap validated concepts in Value Objects (`Email`, `UserId`)
|
|
225
|
+
- Mutable domain models (`var` fields) — always `val` + `copy()`
|
|
226
|
+
- Domain model inheriting from framework class — domain is pure Kotlin only
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Related Skills
|
|
231
|
+
|
|
232
|
+
- `entity-design` — designing Room entities that map to domain models
|
|
233
|
+
- `value-object` — in-depth Value Object patterns
|
|
234
|
+
- `clean-architecture` — where domain sits in the layer structure
|
|
235
|
+
- `dto-mapping` — mapping between domain and data layer models
|
|
236
|
+
- `use-case-design` — business operations that act on domain models
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: entity-design
|
|
3
|
+
description: >
|
|
4
|
+
Designing Room database entities for Android.
|
|
5
|
+
Load this skill when defining @Entity classes, choosing primary keys,
|
|
6
|
+
modeling relationships (one-to-many, many-to-many), using type converters,
|
|
7
|
+
handling nullable columns, or mapping between domain models and entities.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Entity Design
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Room entities are data classes annotated with `@Entity` that map directly to SQLite tables. Entities live in the Data layer and must never leak into Domain or Presentation. The mapping between Entity and Domain model is always explicit.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Entities live in **Data layer only** — never imported by Domain or Presentation
|
|
21
|
+
- Entity fields map to **SQLite columns** — use types SQLite supports natively or via TypeConverter
|
|
22
|
+
- Every entity has a **primary key** — prefer `String` UUID over auto-generated `Int` for distributed data
|
|
23
|
+
- **Nullable columns** only when the data is truly optional — not as a lazy default
|
|
24
|
+
- Entity names reflect **storage** — Domain names reflect **business concepts**
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Basic Entity
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ Standard entity
|
|
32
|
+
@Entity(tableName = "users")
|
|
33
|
+
data class UserEntity(
|
|
34
|
+
@PrimaryKey val id: String,
|
|
35
|
+
@ColumnInfo(name = "full_name") val name: String,
|
|
36
|
+
@ColumnInfo(name = "email_address") val email: String,
|
|
37
|
+
@ColumnInfo(name = "role") val role: String, // store enum as String
|
|
38
|
+
@ColumnInfo(name = "is_active") val isActive: Boolean,
|
|
39
|
+
@ColumnInfo(name = "created_at") val createdAt: Long, // store Instant as epoch millis
|
|
40
|
+
@ColumnInfo(name = "updated_at") val updatedAt: Long
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// ✅ Mapping to/from domain
|
|
44
|
+
fun UserEntity.toDomain(): User = User(
|
|
45
|
+
id = id,
|
|
46
|
+
name = name,
|
|
47
|
+
email = Email(email),
|
|
48
|
+
role = UserRole.valueOf(role),
|
|
49
|
+
status = if (isActive) UserStatus.ACTIVE else UserStatus.SUSPENDED,
|
|
50
|
+
createdAt = Instant.ofEpochMilli(createdAt)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
fun User.toEntity(): UserEntity = UserEntity(
|
|
54
|
+
id = id,
|
|
55
|
+
name = name,
|
|
56
|
+
email = email.value,
|
|
57
|
+
role = role.name,
|
|
58
|
+
isActive = status == UserStatus.ACTIVE,
|
|
59
|
+
createdAt = createdAt.toEpochMilli(),
|
|
60
|
+
updatedAt = System.currentTimeMillis()
|
|
61
|
+
)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Primary Keys
|
|
67
|
+
|
|
68
|
+
```kotlin
|
|
69
|
+
// ✅ String UUID — preferred for distributed/synced data
|
|
70
|
+
@Entity(tableName = "orders")
|
|
71
|
+
data class OrderEntity(
|
|
72
|
+
@PrimaryKey val id: String = UUID.randomUUID().toString(),
|
|
73
|
+
...
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// ✅ Auto-increment Int — for local-only data with no sync
|
|
77
|
+
@Entity(tableName = "drafts")
|
|
78
|
+
data class DraftEntity(
|
|
79
|
+
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
|
80
|
+
val content: String,
|
|
81
|
+
val createdAt: Long
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// ✅ Composite primary key
|
|
85
|
+
@Entity(
|
|
86
|
+
tableName = "user_roles",
|
|
87
|
+
primaryKeys = ["user_id", "role_id"]
|
|
88
|
+
)
|
|
89
|
+
data class UserRoleEntity(
|
|
90
|
+
@ColumnInfo(name = "user_id") val userId: String,
|
|
91
|
+
@ColumnInfo(name = "role_id") val roleId: String,
|
|
92
|
+
@ColumnInfo(name = "assigned_at") val assignedAt: Long
|
|
93
|
+
)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Relationships
|
|
99
|
+
|
|
100
|
+
```kotlin
|
|
101
|
+
// ✅ One-to-many: Order has many OrderItems
|
|
102
|
+
@Entity(tableName = "orders")
|
|
103
|
+
data class OrderEntity(
|
|
104
|
+
@PrimaryKey val id: String,
|
|
105
|
+
@ColumnInfo(name = "customer_id") val customerId: String,
|
|
106
|
+
@ColumnInfo(name = "status") val status: String,
|
|
107
|
+
@ColumnInfo(name = "placed_at") val placedAt: Long
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@Entity(
|
|
111
|
+
tableName = "order_items",
|
|
112
|
+
foreignKeys = [
|
|
113
|
+
ForeignKey(
|
|
114
|
+
entity = OrderEntity::class,
|
|
115
|
+
parentColumns = ["id"],
|
|
116
|
+
childColumns = ["order_id"],
|
|
117
|
+
onDelete = ForeignKey.CASCADE // delete items when order deleted
|
|
118
|
+
)
|
|
119
|
+
],
|
|
120
|
+
indices = [Index("order_id")] // ✅ index foreign key columns
|
|
121
|
+
)
|
|
122
|
+
data class OrderItemEntity(
|
|
123
|
+
@PrimaryKey val id: String,
|
|
124
|
+
@ColumnInfo(name = "order_id") val orderId: String,
|
|
125
|
+
@ColumnInfo(name = "product_id") val productId: String,
|
|
126
|
+
@ColumnInfo(name = "quantity") val quantity: Int,
|
|
127
|
+
@ColumnInfo(name = "unit_price") val unitPrice: Long
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
// ✅ Relation data class for queries
|
|
131
|
+
data class OrderWithItems(
|
|
132
|
+
@Embedded val order: OrderEntity,
|
|
133
|
+
@Relation(
|
|
134
|
+
parentColumn = "id",
|
|
135
|
+
entityColumn = "order_id"
|
|
136
|
+
)
|
|
137
|
+
val items: List<OrderItemEntity>
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// ✅ Many-to-many via junction table
|
|
141
|
+
@Entity(
|
|
142
|
+
tableName = "product_tags",
|
|
143
|
+
primaryKeys = ["product_id", "tag_id"]
|
|
144
|
+
)
|
|
145
|
+
data class ProductTagCrossRef(
|
|
146
|
+
@ColumnInfo(name = "product_id") val productId: String,
|
|
147
|
+
@ColumnInfo(name = "tag_id") val tagId: String
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
data class ProductWithTags(
|
|
151
|
+
@Embedded val product: ProductEntity,
|
|
152
|
+
@Relation(
|
|
153
|
+
parentColumn = "id",
|
|
154
|
+
entityColumn = "id",
|
|
155
|
+
associateBy = Junction(ProductTagCrossRef::class,
|
|
156
|
+
parentColumn = "product_id",
|
|
157
|
+
entityColumn = "tag_id")
|
|
158
|
+
)
|
|
159
|
+
val tags: List<TagEntity>
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Type Converters
|
|
166
|
+
|
|
167
|
+
```kotlin
|
|
168
|
+
// ✅ TypeConverter for types Room can't store natively
|
|
169
|
+
class Converters {
|
|
170
|
+
|
|
171
|
+
// List<String>
|
|
172
|
+
@TypeConverter
|
|
173
|
+
fun fromStringList(value: List<String>): String = value.joinToString(",")
|
|
174
|
+
|
|
175
|
+
@TypeConverter
|
|
176
|
+
fun toStringList(value: String): List<String> =
|
|
177
|
+
if (value.isBlank()) emptyList() else value.split(",")
|
|
178
|
+
|
|
179
|
+
// Enum (store as String, not ordinal — ordinals break on reorder)
|
|
180
|
+
@TypeConverter
|
|
181
|
+
fun fromUserRole(role: UserRole): String = role.name
|
|
182
|
+
|
|
183
|
+
@TypeConverter
|
|
184
|
+
fun toUserRole(value: String): UserRole = UserRole.valueOf(value)
|
|
185
|
+
|
|
186
|
+
// Instant (store as epoch millis)
|
|
187
|
+
@TypeConverter
|
|
188
|
+
fun fromInstant(instant: Instant?): Long? = instant?.toEpochMilli()
|
|
189
|
+
|
|
190
|
+
@TypeConverter
|
|
191
|
+
fun toInstant(value: Long?): Instant? = value?.let { Instant.ofEpochMilli(it) }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ✅ Register converters on the database
|
|
195
|
+
@Database(entities = [...], version = 1)
|
|
196
|
+
@TypeConverters(Converters::class)
|
|
197
|
+
abstract class AppDatabase : RoomDatabase()
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Indices
|
|
203
|
+
|
|
204
|
+
```kotlin
|
|
205
|
+
// ✅ Index frequently queried columns
|
|
206
|
+
@Entity(
|
|
207
|
+
tableName = "products",
|
|
208
|
+
indices = [
|
|
209
|
+
Index(value = ["category_id"]), // single column
|
|
210
|
+
Index(value = ["name", "category_id"]), // composite
|
|
211
|
+
Index(value = ["sku"], unique = true) // unique constraint
|
|
212
|
+
]
|
|
213
|
+
)
|
|
214
|
+
data class ProductEntity(
|
|
215
|
+
@PrimaryKey val id: String,
|
|
216
|
+
@ColumnInfo(name = "name") val name: String,
|
|
217
|
+
@ColumnInfo(name = "sku") val sku: String,
|
|
218
|
+
@ColumnInfo(name = "category_id") val categoryId: String,
|
|
219
|
+
@ColumnInfo(name = "price") val price: Long
|
|
220
|
+
)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Anti-Patterns
|
|
226
|
+
|
|
227
|
+
- `@Entity` class used in Domain or Presentation layer — entities are Data layer only
|
|
228
|
+
- Storing enum as ordinal (`role.ordinal`) — breaks when enum values are reordered; use `name`
|
|
229
|
+
- Missing `Index` on foreign key columns — causes full table scan on joins
|
|
230
|
+
- `onDelete = ForeignKey.NO_ACTION` on cascading data — leaves orphan rows
|
|
231
|
+
- Using `@Ignore` to skip fields instead of proper mapping — creates confusion about what's stored
|
|
232
|
+
- Storing complex objects as JSON strings without a TypeConverter — makes querying impossible
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Related Skills
|
|
237
|
+
|
|
238
|
+
- `dao` — DAO patterns for querying entities
|
|
239
|
+
- `room` — Room database setup and configuration
|
|
240
|
+
- `migration` — handling entity schema changes
|
|
241
|
+
- `domain-modeling` — domain models that entities map to
|
|
242
|
+
- `dto-mapping` — mapping patterns between layers
|
|
243
|
+
- `indexing` — in-depth index strategy for performance
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feature-isolation
|
|
3
|
+
description: >
|
|
4
|
+
Feature isolation patterns for Android modular projects.
|
|
5
|
+
Load this skill when ensuring features don't leak implementation details,
|
|
6
|
+
designing feature APIs, preventing feature-to-feature coupling,
|
|
7
|
+
or structuring a feature module's internal vs public surface.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Feature Isolation
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Feature isolation ensures that each feature module exposes only a minimal, stable public API while hiding all implementation details as `internal`. Features never depend on each other directly. Cross-feature communication happens via shared interfaces, callbacks, or a shared core module — never via direct imports.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- **`internal` by default** — everything inside a feature is `internal` unless explicitly needed outside
|
|
21
|
+
- **No feature-to-feature imports** — features only import from `:core:*` modules
|
|
22
|
+
- **Public API is minimal** — expose only navigation routes, DI modules, and nav graph extensions
|
|
23
|
+
- **Feature entry points are composable extensions** — `NavGraphBuilder.featureGraph()`
|
|
24
|
+
- **Callbacks over navigation coupling** — features receive lambdas for cross-feature navigation
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Feature Public Surface
|
|
29
|
+
|
|
30
|
+
```kotlin
|
|
31
|
+
// ✅ What a feature exposes (public)
|
|
32
|
+
// feature/orders/src/main/kotlin/.../
|
|
33
|
+
|
|
34
|
+
// 1. Navigation contract
|
|
35
|
+
object OrdersNavigation {
|
|
36
|
+
const val graphRoute = "orders_graph"
|
|
37
|
+
const val listRoute = "orders/list"
|
|
38
|
+
const val detailRoute = "orders/detail/{orderId}"
|
|
39
|
+
|
|
40
|
+
fun detailRoute(orderId: String) = "orders/detail/$orderId"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 2. NavGraph extension — entry point for :app
|
|
44
|
+
fun NavGraphBuilder.ordersGraph(
|
|
45
|
+
navController: NavController,
|
|
46
|
+
onNavigateToProduct: (productId: String) -> Unit, // cross-feature via callback
|
|
47
|
+
onNavigateToProfile: () -> Unit
|
|
48
|
+
) {
|
|
49
|
+
navigation(
|
|
50
|
+
startDestination = OrdersNavigation.listRoute,
|
|
51
|
+
route = OrdersNavigation.graphRoute
|
|
52
|
+
) {
|
|
53
|
+
composable(OrdersNavigation.listRoute) {
|
|
54
|
+
OrderListScreen( // internal composable
|
|
55
|
+
onOrderClick = { orderId ->
|
|
56
|
+
navController.navigate(OrdersNavigation.detailRoute(orderId))
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
composable(
|
|
61
|
+
route = OrdersNavigation.detailRoute,
|
|
62
|
+
arguments = listOf(navArgument("orderId") { type = NavType.StringType })
|
|
63
|
+
) { backStackEntry ->
|
|
64
|
+
val orderId = backStackEntry.arguments?.getString("orderId") ?: return@composable
|
|
65
|
+
OrderDetailScreen(
|
|
66
|
+
orderId = orderId,
|
|
67
|
+
onNavigateToProduct = onNavigateToProduct,
|
|
68
|
+
onNavigateToProfile = onNavigateToProfile
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 3. Hilt module (internal — bindings are auto-discovered by Hilt)
|
|
75
|
+
@Module
|
|
76
|
+
@InstallIn(SingletonComponent::class)
|
|
77
|
+
internal abstract class OrdersModule {
|
|
78
|
+
@Binds
|
|
79
|
+
abstract fun bindOrderRepository(impl: OrderRepositoryImpl): OrderRepository
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Internal Implementation
|
|
86
|
+
|
|
87
|
+
```kotlin
|
|
88
|
+
// ✅ Everything implementation-related is internal
|
|
89
|
+
internal class OrderRepositoryImpl @Inject constructor(
|
|
90
|
+
private val dao: OrderDao,
|
|
91
|
+
private val api: OrderApiService
|
|
92
|
+
) : OrderRepository { ... }
|
|
93
|
+
|
|
94
|
+
internal class GetOrdersUseCase @Inject constructor(
|
|
95
|
+
private val repository: OrderRepository
|
|
96
|
+
) {
|
|
97
|
+
operator fun invoke(): Flow<List<Order>> = repository.observeOrders()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@HiltViewModel
|
|
101
|
+
internal class OrderListViewModel @Inject constructor(
|
|
102
|
+
private val getOrdersUseCase: GetOrdersUseCase
|
|
103
|
+
) : ViewModel() { ... }
|
|
104
|
+
|
|
105
|
+
@Composable
|
|
106
|
+
internal fun OrderListScreen(onOrderClick: (String) -> Unit) { ... }
|
|
107
|
+
|
|
108
|
+
@Composable
|
|
109
|
+
internal fun OrderDetailScreen(
|
|
110
|
+
orderId: String,
|
|
111
|
+
onNavigateToProduct: (String) -> Unit,
|
|
112
|
+
onNavigateToProfile: () -> Unit
|
|
113
|
+
) { ... }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Cross-Feature Communication Patterns
|
|
119
|
+
|
|
120
|
+
```kotlin
|
|
121
|
+
// ✅ Pattern 1: Callbacks (simplest — for navigation)
|
|
122
|
+
// :app wires the callback
|
|
123
|
+
ordersGraph(
|
|
124
|
+
navController = navController,
|
|
125
|
+
onNavigateToProduct = { productId ->
|
|
126
|
+
navController.navigate(CatalogNavigation.detailRoute(productId))
|
|
127
|
+
},
|
|
128
|
+
onNavigateToProfile = {
|
|
129
|
+
navController.navigate(ProfileNavigation.graphRoute)
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// ✅ Pattern 2: Shared interface in :core:domain (for data)
|
|
134
|
+
// :core:domain
|
|
135
|
+
interface ProductInfoProvider {
|
|
136
|
+
suspend fun getProductSummary(productId: String): Result<ProductSummary>
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// :feature:catalog implements it
|
|
140
|
+
internal class ProductInfoProviderImpl @Inject constructor(
|
|
141
|
+
private val repository: ProductRepository
|
|
142
|
+
) : ProductInfoProvider { ... }
|
|
143
|
+
|
|
144
|
+
// :feature:orders uses it (no knowledge of :feature:catalog)
|
|
145
|
+
internal class OrderDetailViewModel @Inject constructor(
|
|
146
|
+
private val productInfoProvider: ProductInfoProvider // injected, not imported
|
|
147
|
+
) : ViewModel() { ... }
|
|
148
|
+
|
|
149
|
+
// ✅ Pattern 3: Shared ViewModel in :app (for truly global state)
|
|
150
|
+
// :app — AuthViewModel scoped to root nav graph
|
|
151
|
+
@Composable
|
|
152
|
+
fun AppNavHost(navController: NavHostController) {
|
|
153
|
+
val appViewModel: AppViewModel = hiltViewModel() // scoped to root
|
|
154
|
+
val authState by appViewModel.authState.collectAsStateWithLifecycle()
|
|
155
|
+
|
|
156
|
+
ordersGraph(
|
|
157
|
+
navController = navController,
|
|
158
|
+
currentUserId = authState.userId // passed down as param
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Feature API Contract Checklist
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
✅ Public (exposed to :app):
|
|
169
|
+
- Navigation object with route constants
|
|
170
|
+
- NavGraphBuilder extension function
|
|
171
|
+
- Hilt module (auto-discovered, but still internal annotation on bindings)
|
|
172
|
+
|
|
173
|
+
❌ Must be internal:
|
|
174
|
+
- All ViewModel classes
|
|
175
|
+
- All composable functions
|
|
176
|
+
- All UseCase classes
|
|
177
|
+
- All Repository implementations
|
|
178
|
+
- All DAO and API service classes
|
|
179
|
+
- All domain model classes (unless in :core:domain)
|
|
180
|
+
- All Hilt @Binds/@Provides methods
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Detecting Isolation Violations
|
|
186
|
+
|
|
187
|
+
```kotlin
|
|
188
|
+
// ❌ Violation: feature:cart importing from feature:catalog
|
|
189
|
+
import com.example.feature.catalog.domain.model.Product // WRONG
|
|
190
|
+
|
|
191
|
+
// ✅ Fix: define what cart needs in :core:domain or via an interface
|
|
192
|
+
// :core:domain
|
|
193
|
+
data class ProductSummary(val id: String, val name: String, val price: Money)
|
|
194
|
+
|
|
195
|
+
// :feature:cart uses ProductSummary — :feature:catalog maps to it
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Anti-Patterns
|
|
201
|
+
|
|
202
|
+
- `public` on internal ViewModels or composables — leaks implementation, creates coupling
|
|
203
|
+
- Feature importing another feature's ViewModel — use shared ViewModel in :app instead
|
|
204
|
+
- Passing navigation controller deep into composables — use callbacks instead
|
|
205
|
+
- Shared mutable state between features via static/companion objects — use proper DI scope
|
|
206
|
+
- One feature calling another feature's UseCase directly — go through shared interface in :core
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Related Skills
|
|
211
|
+
|
|
212
|
+
- `modularization` — module structure that enables isolation
|
|
213
|
+
- `multi-module-architecture` — full multi-module setup
|
|
214
|
+
- `bounded-context` — conceptual boundaries between features
|
|
215
|
+
- `hilt` — scoping DI across modules correctly
|
|
216
|
+
- `navigation` — wiring navigation between isolated features
|