android-sdd 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/dist/index.js +143 -0
  2. package/package.json +27 -0
  3. package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
  4. package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
  5. package/skills/Android Platform/Configuration/SKILL.md +201 -0
  6. package/skills/Android Platform/Filesystem/SKILL.md +216 -0
  7. package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
  8. package/skills/Android Platform/Manifest/SKILL.md +226 -0
  9. package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
  10. package/skills/Android Platform/Resources/SKILL.md +234 -0
  11. package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
  12. package/skills/Android Platform/State Restoration/SKILL.md +210 -0
  13. package/skills/Architecture/Bounded Context/SKILL.md +207 -0
  14. package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
  15. package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
  16. package/skills/Architecture/Entity Design/SKILL.md +243 -0
  17. package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
  18. package/skills/Architecture/MVI/SKILL.md +224 -0
  19. package/skills/Architecture/MVVM/SKILL.md +198 -0
  20. package/skills/Architecture/Modularization/SKILL.md +194 -0
  21. package/skills/Architecture/Offline First/SKILL.md +249 -0
  22. package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
  23. package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
  24. package/skills/Architecture/State Management/SKILL.md +229 -0
  25. package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
  26. package/skills/Architecture/Use Case Design/SKILL.md +244 -0
  27. package/skills/Architecture/Value Object/SKILL.md +226 -0
  28. package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
  29. package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
  30. package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
  31. package/skills/Build System/Build Cache/SKILL.md +233 -0
  32. package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
  33. package/skills/Build System/Build Variant/SKILL.md +215 -0
  34. package/skills/Build System/Convention Plugin/SKILL.md +288 -0
  35. package/skills/Build System/Dependency Management/SKILL.md +261 -0
  36. package/skills/Build System/Gradle/SKILL.md +284 -0
  37. package/skills/Build System/Incremental Build/SKILL.md +199 -0
  38. package/skills/Build System/KAPT/SKILL.md +198 -0
  39. package/skills/Build System/KSP/SKILL.md +263 -0
  40. package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
  41. package/skills/Build System/Specialized/C++/SKILL.md +308 -0
  42. package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
  43. package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
  44. package/skills/Build System/Version Catalog/SKILL.md +304 -0
  45. package/skills/Concurrency/Background Processing/SKILL.md +185 -0
  46. package/skills/Concurrency/Channel/SKILL.md +207 -0
  47. package/skills/Concurrency/Coroutine/SKILL.md +200 -0
  48. package/skills/Concurrency/Flow/SKILL.md +179 -0
  49. package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
  50. package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
  51. package/skills/Concurrency/StateFlow/SKILL.md +175 -0
  52. package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
  53. package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
  54. package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
  55. package/skills/Core Language/DSL/SKILL.md +186 -0
  56. package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
  57. package/skills/Core Language/Immutability/SKILL.md +156 -0
  58. package/skills/Core Language/KMP/SKILL.md +182 -0
  59. package/skills/Core Language/Kotlin/SKILL.md +187 -0
  60. package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
  61. package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
  62. package/skills/Core Language/Serialization/SKILL.md +191 -0
  63. package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
  64. package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
  65. package/skills/Data Layer/DAO/SKILL.md +225 -0
  66. package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
  67. package/skills/Data Layer/DataStore/SKILL.md +264 -0
  68. package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
  69. package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
  70. package/skills/Data Layer/File Storage/SKILL.md +247 -0
  71. package/skills/Data Layer/Indexing/SKILL.md +184 -0
  72. package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
  73. package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
  74. package/skills/Data Layer/Migration/SKILL.md +243 -0
  75. package/skills/Data Layer/Paging/SKILL.md +264 -0
  76. package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
  77. package/skills/Data Layer/Room/SKILL.md +244 -0
  78. package/skills/Data Layer/SQLite/SKILL.md +255 -0
  79. package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
  80. package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
  81. package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
  82. package/skills/Dependency Injection/Koin/SKILL.md +282 -0
  83. package/skills/Developer Experience/Detekt/SKILL.md +272 -0
  84. package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
  85. package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
  86. package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
  87. package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
  88. package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
  89. package/skills/Media/Audio/SKILL.md +257 -0
  90. package/skills/Media/Camera/SKILL.md +229 -0
  91. package/skills/Media/CameraX/SKILL.md +295 -0
  92. package/skills/Media/ExoPlayer/SKILL.md +258 -0
  93. package/skills/Media/Video/SKILL.md +228 -0
  94. package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
  95. package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
  96. package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
  97. package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
  98. package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
  99. package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
  100. package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
  101. package/skills/Navigation/Navigation/SKILL.md +215 -0
  102. package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
  103. package/skills/Networking/API Contract/SKILL.md +220 -0
  104. package/skills/Networking/Authentication/SKILL.md +210 -0
  105. package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
  106. package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
  107. package/skills/Networking/Ktor/SKILL.md +219 -0
  108. package/skills/Networking/Multipart Upload/SKILL.md +213 -0
  109. package/skills/Networking/OkHttp/SKILL.md +193 -0
  110. package/skills/Networking/REST/SKILL.md +178 -0
  111. package/skills/Networking/Rate Limiting/SKILL.md +170 -0
  112. package/skills/Networking/Retrofit/SKILL.md +241 -0
  113. package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
  114. package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
  115. package/skills/Networking/WebSocket/SKILL.md +224 -0
  116. package/skills/Observability/Crash Reporting/SKILL.md +219 -0
  117. package/skills/Observability/Logging/SKILL.md +168 -0
  118. package/skills/Observability/Metrics/SKILL.md +227 -0
  119. package/skills/Observability/Structured Logging/SKILL.md +234 -0
  120. package/skills/Performance/ANR Prevention/SKILL.md +192 -0
  121. package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
  122. package/skills/Performance/App Startup/SKILL.md +183 -0
  123. package/skills/Performance/Baseline Profile/SKILL.md +205 -0
  124. package/skills/Performance/Battery Optimization/SKILL.md +192 -0
  125. package/skills/Performance/Benchmark/SKILL.md +182 -0
  126. package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
  127. package/skills/Performance/Compose Optimization/SKILL.md +187 -0
  128. package/skills/Performance/Heap Management/SKILL.md +184 -0
  129. package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
  130. package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
  131. package/skills/Performance/Rendering Performance/SKILL.md +205 -0
  132. package/skills/Performance/Startup Optimization/SKILL.md +219 -0
  133. package/skills/Security/Biometric/SKILL.md +224 -0
  134. package/skills/Security/Certificate Transparency/SKILL.md +158 -0
  135. package/skills/Security/Cryptography/SKILL.md +244 -0
  136. package/skills/Security/Encrypted Storage/SKILL.md +273 -0
  137. package/skills/Security/Frida Detection/SKILL.md +230 -0
  138. package/skills/Security/Hook Detection/SKILL.md +197 -0
  139. package/skills/Security/Keystore/SKILL.md +272 -0
  140. package/skills/Security/Network Security Config/SKILL.md +186 -0
  141. package/skills/Security/Obfuscation/SKILL.md +226 -0
  142. package/skills/Security/Proguard/SKILL.md +202 -0
  143. package/skills/Security/R8/SKILL.md +234 -0
  144. package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
  145. package/skills/Security/Root Detection/SKILL.md +220 -0
  146. package/skills/Security/Secure Networking/SKILL.md +220 -0
  147. package/skills/System Integration/AlarmManager/SKILL.md +182 -0
  148. package/skills/System Integration/App Widget/SKILL.md +182 -0
  149. package/skills/System Integration/Deep Link/SKILL.md +187 -0
  150. package/skills/System Integration/Foreground Service/SKILL.md +212 -0
  151. package/skills/System Integration/Notification/SKILL.md +237 -0
  152. package/skills/System Integration/WorkManager/SKILL.md +256 -0
  153. package/skills/System Integration/clipboard/SKILL.md +155 -0
  154. package/skills/System Integration/share-intent/SKILL.md +182 -0
  155. package/skills/Testing/Compose Testing/SKILL.md +296 -0
  156. package/skills/Testing/Espresso/SKILL.md +292 -0
  157. package/skills/Testing/Fake Data/SKILL.md +245 -0
  158. package/skills/Testing/Integration Testing/SKILL.md +288 -0
  159. package/skills/Testing/Mocking/SKILL.md +229 -0
  160. package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
  161. package/skills/Testing/UI Testing/SKILL.md +293 -0
  162. package/skills/Testing/Unit Testing/SKILL.md +309 -0
  163. package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
  164. package/skills/UI System/Compose/SKILL.md +296 -0
  165. package/skills/UI System/Compose Animation/SKILL.md +281 -0
  166. package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
  167. package/skills/UI System/Compose Navigation/SKILL.md +255 -0
  168. package/skills/UI System/Compose Performance/SKILL.md +274 -0
  169. package/skills/UI System/Design System/SKILL.md +217 -0
  170. package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
  171. package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
  172. package/skills/UI System/Loading Strategy/SKILL.md +254 -0
  173. package/skills/UI System/Material 3/SKILL.md +279 -0
  174. package/skills/UI System/RTL/SKILL.md +179 -0
  175. package/src/index.ts +182 -0
  176. package/tsconfig.json +19 -0
@@ -0,0 +1,182 @@
1
+ ---
2
+ name: fallback-strategy
3
+ description: >
4
+ Fallback strategies for handling network and data failures in Android.
5
+ Load this skill when deciding what to show after all retries are exhausted,
6
+ serving stale cache on network failure, implementing graceful degradation,
7
+ or building resilient offline-capable features.
8
+ ---
9
+
10
+ # Fallback Strategy
11
+
12
+ ## Overview
13
+ A fallback strategy defines what the app does when the primary data source fails. Instead of showing an error and stopping, the app degrades gracefully — serving cached data, a default value, or a reduced-functionality state. The goal is to keep the user productive even when connectivity or the server is unavailable.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - Always try the **fastest/most reliable source first** — then fall back in order
20
+ - Serve **stale data with a staleness indicator** rather than a hard error when possible
21
+ - Fallback order: memory cache → disk cache → network → default/empty
22
+ - Never silently serve stale data without indicating it to the user
23
+ - Fallback behavior must be **explicit in the repository** — not implicit
24
+
25
+ ---
26
+
27
+ ## Fallback Chain Pattern
28
+
29
+ ```kotlin
30
+ // ✅ Explicit fallback chain in repository
31
+ class UserRepositoryImpl @Inject constructor(
32
+ private val remoteSource: UserRemoteDataSource,
33
+ private val localSource: UserLocalDataSource,
34
+ private val memoryCache: UserMemoryCache
35
+ ) : UserRepository {
36
+
37
+ override suspend fun getUser(id: String): Result<User> {
38
+ // 1. Memory cache
39
+ memoryCache.get(id)?.let { return Result.success(it) }
40
+
41
+ // 2. Network
42
+ val networkResult = runCatching { remoteSource.getUser(id) }
43
+ if (networkResult.isSuccess) {
44
+ val user = networkResult.getOrThrow()
45
+ memoryCache.put(id, user)
46
+ localSource.saveUser(user)
47
+ return Result.success(user)
48
+ }
49
+
50
+ // 3. Disk cache fallback
51
+ val cached = localSource.getUser(id)
52
+ if (cached != null) {
53
+ return Result.success(cached.copy(isStale = true))
54
+ }
55
+
56
+ // 4. All sources failed
57
+ return networkResult // propagate original error
58
+ }
59
+ }
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Stale Data Model
65
+
66
+ ```kotlin
67
+ // ✅ Domain model carries staleness flag
68
+ data class User(
69
+ val id: String,
70
+ val name: String,
71
+ val email: String,
72
+ val isStale: Boolean = false // true when served from cache after network failure
73
+ )
74
+
75
+ // ✅ UI shows staleness indicator
76
+ @Composable
77
+ fun UserDetailScreen(state: UserDetailUiState) {
78
+ if (state is UserDetailUiState.Success) {
79
+ if (state.user.isStale) {
80
+ StaleBanner(message = "Showing cached data — pull to refresh")
81
+ }
82
+ UserContent(state.user)
83
+ }
84
+ }
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Network-First with Cache Fallback (Flow)
90
+
91
+ ```kotlin
92
+ // ✅ Emit cached data immediately, then update with network
93
+ override fun getUserStream(id: String): Flow<Result<User>> = flow {
94
+ // Emit cached immediately
95
+ val cached = localSource.getUser(id)
96
+ if (cached != null) emit(Result.success(cached.copy(isStale = true)))
97
+
98
+ // Fetch fresh from network
99
+ runCatching { remoteSource.getUser(id) }
100
+ .onSuccess { fresh ->
101
+ localSource.saveUser(fresh)
102
+ emit(Result.success(fresh))
103
+ }
104
+ .onFailure { error ->
105
+ if (cached == null) emit(Result.failure(error))
106
+ // else already emitted stale — don't emit error
107
+ }
108
+ }
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Default Value Fallback
114
+
115
+ ```kotlin
116
+ // ✅ Return sensible defaults when data is unavailable
117
+ override suspend fun getAppConfig(): AppConfig {
118
+ return runCatching { remoteSource.getConfig() }
119
+ .getOrElse {
120
+ localSource.getConfig() ?: AppConfig.default()
121
+ }
122
+ }
123
+
124
+ // ✅ Default config
125
+ data class AppConfig(
126
+ val featureFlags: Map<String, Boolean> = emptyMap(),
127
+ val maxUploadSize: Long = 10 * 1024 * 1024L // 10MB
128
+ ) {
129
+ companion object {
130
+ fun default() = AppConfig()
131
+ }
132
+ }
133
+ ```
134
+
135
+ ---
136
+
137
+ ## Partial Fallback (Feature Degradation)
138
+
139
+ ```kotlin
140
+ // ✅ Load what's available — degrade features that can't load
141
+ data class DashboardData(
142
+ val user: User,
143
+ val recentOrders: List<Order> = emptyList(), // empty = orders unavailable
144
+ val notifications: List<Notification> = emptyList(),
145
+ val ordersError: Boolean = false,
146
+ val notificationsError: Boolean = false
147
+ )
148
+
149
+ suspend fun loadDashboard(userId: String): DashboardData {
150
+ val user = userRepository.getUser(userId).getOrThrow() // required — throw if fails
151
+
152
+ val orders = orderRepository.getRecentOrders(userId)
153
+ val notifications = notificationRepository.getUnread(userId)
154
+
155
+ return DashboardData(
156
+ user = user,
157
+ recentOrders = orders.getOrDefault(emptyList()),
158
+ notifications = notifications.getOrDefault(emptyList()),
159
+ ordersError = orders.isFailure,
160
+ notificationsError = notifications.isFailure
161
+ )
162
+ }
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Anti-Patterns
168
+
169
+ - Showing a hard error screen when cached data is available — use stale data instead
170
+ - Silently serving stale data without any indicator — user doesn't know data may be old
171
+ - Retrying indefinitely before falling back — set a max retry then fall back
172
+ - Treating all fallback data the same as fresh data — mark staleness explicitly
173
+ - Fallback logic scattered across ViewModel and Repository — keep it in the repository
174
+
175
+ ---
176
+
177
+ ## Related Skills
178
+ - `retry-backoff` — exhausting retries before triggering fallback
179
+ - `cache-strategy` — cache implementation details
180
+ - `offline-first` — building apps that work without connectivity
181
+ - `error-handling` — propagating errors when all fallbacks fail
182
+ - `loading-strategy` — showing appropriate loading states during fallback
@@ -0,0 +1,219 @@
1
+ ---
2
+ name: ktor
3
+ description: >
4
+ Ktor HTTP client setup and usage for Android and KMP projects.
5
+ Load this skill when using Ktor as the HTTP client, configuring
6
+ plugins, defining typed API calls, handling responses, or building
7
+ a shared networking layer in a Kotlin Multiplatform project.
8
+ ---
9
+
10
+ # Ktor
11
+
12
+ ## Overview
13
+ Ktor Client is a multiplatform HTTP client built on coroutines. It is the preferred choice for KMP projects where the networking layer is shared between Android and iOS. It uses a plugin-based architecture for features like serialization, auth, logging, and retry.
14
+
15
+ ---
16
+
17
+ ## Core Principles
18
+
19
+ - Use Ktor when the networking layer must be shared across platforms (KMP)
20
+ - Use Retrofit for Android-only projects — Ktor for KMP
21
+ - Configure one `HttpClient` instance per app — provide via DI
22
+ - All requests are `suspend` functions — no callbacks
23
+ - Handle errors at the repository level — wrap in `Result`
24
+
25
+ ---
26
+
27
+ ## Setup
28
+
29
+ ```toml
30
+ # libs.versions.toml
31
+ [versions]
32
+ ktor = "2.3.12"
33
+
34
+ [libraries]
35
+ ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
36
+ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } # Android
37
+ ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } # iOS
38
+ ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
39
+ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
40
+ ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
41
+ ktor-client-auth = { module = "io.ktor:ktor-client-auth", version.ref = "ktor" }
42
+ ```
43
+
44
+ ---
45
+
46
+ ## Client Configuration
47
+
48
+ ```kotlin
49
+ // ✅ Shared HttpClient — configured once
50
+ fun createHttpClient(json: Json): HttpClient {
51
+ return HttpClient(OkHttp) { // use Darwin engine on iOS
52
+ // Serialization
53
+ install(ContentNegotiation) {
54
+ json(json)
55
+ }
56
+
57
+ // Logging
58
+ install(Logging) {
59
+ logger = object : Logger {
60
+ override fun log(message: String) {
61
+ println("Ktor: $message")
62
+ }
63
+ }
64
+ level = if (BuildConfig.DEBUG) LogLevel.BODY else LogLevel.NONE
65
+ }
66
+
67
+ // Auth
68
+ install(Auth) {
69
+ bearer {
70
+ loadTokens {
71
+ BearerTokens(
72
+ accessToken = tokenStorage.getAccessToken() ?: "",
73
+ refreshToken = tokenStorage.getRefreshToken() ?: ""
74
+ )
75
+ }
76
+ refreshTokens {
77
+ val newTokens = tokenApi.refresh(oldTokens?.refreshToken ?: "")
78
+ tokenStorage.save(newTokens)
79
+ BearerTokens(newTokens.accessToken, newTokens.refreshToken)
80
+ }
81
+ }
82
+ }
83
+
84
+ // Default request config
85
+ defaultRequest {
86
+ url(BuildConfig.BASE_URL)
87
+ contentType(ContentType.Application.Json)
88
+ header("X-Platform", "android")
89
+ }
90
+
91
+ // Timeouts
92
+ install(HttpTimeout) {
93
+ requestTimeoutMillis = 30_000
94
+ connectTimeoutMillis = 15_000
95
+ socketTimeoutMillis = 30_000
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ---
102
+
103
+ ## API Calls
104
+
105
+ ```kotlin
106
+ // ✅ Typed API functions using Ktor DSL
107
+ class UserRemoteDataSource @Inject constructor(
108
+ private val client: HttpClient
109
+ ) {
110
+ suspend fun getUsers(): List<UserDto> {
111
+ return client.get("users").body()
112
+ }
113
+
114
+ suspend fun getUser(id: String): UserDto {
115
+ return client.get("users/$id").body()
116
+ }
117
+
118
+ suspend fun createUser(request: CreateUserRequest): UserDto {
119
+ return client.post("users") {
120
+ setBody(request)
121
+ }.body()
122
+ }
123
+
124
+ suspend fun updateUser(id: String, request: UpdateUserRequest): UserDto {
125
+ return client.put("users/$id") {
126
+ setBody(request)
127
+ }.body()
128
+ }
129
+
130
+ suspend fun deleteUser(id: String) {
131
+ client.delete("users/$id")
132
+ }
133
+
134
+ suspend fun searchUsers(query: String, page: Int): PagedResponse<UserDto> {
135
+ return client.get("users") {
136
+ parameter("q", query)
137
+ parameter("page", page)
138
+ }.body()
139
+ }
140
+ }
141
+ ```
142
+
143
+ ---
144
+
145
+ ## Error Handling
146
+
147
+ ```kotlin
148
+ // ✅ Wrap Ktor calls in runCatching at the repository level
149
+ override suspend fun getUser(id: String): Result<User> = runCatching {
150
+ val dto = remoteDataSource.getUser(id)
151
+ mapper.toDomain(dto)
152
+ }.recoverCatching { throwable ->
153
+ when (throwable) {
154
+ is ClientRequestException -> throw ApiException(
155
+ code = throwable.response.status.value,
156
+ message = throwable.response.bodyAsText()
157
+ )
158
+ is ServerResponseException -> throw ServerException(
159
+ code = throwable.response.status.value
160
+ )
161
+ is IOException -> throw NetworkException("No internet")
162
+ else -> throw throwable
163
+ }
164
+ }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## File Upload
170
+
171
+ ```kotlin
172
+ // ✅ Multipart upload with Ktor
173
+ suspend fun uploadAvatar(userId: String, file: ByteArray, fileName: String): AvatarDto {
174
+ return client.submitFormWithBinaryData(
175
+ url = "users/$userId/avatar",
176
+ formData = formData {
177
+ append("avatar", file, Headers.build {
178
+ append(HttpHeaders.ContentType, "image/jpeg")
179
+ append(HttpHeaders.ContentDisposition, "filename=$fileName")
180
+ })
181
+ }
182
+ ).body()
183
+ }
184
+ ```
185
+
186
+ ---
187
+
188
+ ## KMP Engine Selection
189
+
190
+ ```kotlin
191
+ // ✅ Expect/actual for engine selection in KMP
192
+ // commonMain
193
+ expect fun createEngine(): HttpClientEngine
194
+
195
+ // androidMain
196
+ actual fun createEngine(): HttpClientEngine = OkHttp.create()
197
+
198
+ // iosMain
199
+ actual fun createEngine(): HttpClientEngine = Darwin.create()
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Anti-Patterns
205
+
206
+ - Using Ktor in Android-only projects when Retrofit is simpler and more established
207
+ - Creating a new `HttpClient` per request — expensive, use singleton
208
+ - Not installing `HttpTimeout` — requests can hang indefinitely
209
+ - Catching exceptions in the data source — wrap at repository level
210
+ - Using string concatenation for URL building — use `parameter()` and path segments
211
+
212
+ ---
213
+
214
+ ## Related Skills
215
+ - `retrofit` — alternative for Android-only projects
216
+ - `serialization` — Kotlinx Serialization shared Json instance
217
+ - `kmp` — KMP project structure and engine selection
218
+ - `authentication` — token management with Ktor Auth plugin
219
+ - `retry-backoff` — retry plugin for Ktor
@@ -0,0 +1,213 @@
1
+ ---
2
+ name: multipart-upload
3
+ description: >
4
+ Multipart file upload in Android using Retrofit or Ktor.
5
+ Load this skill when uploading images, videos, or files to a server,
6
+ tracking upload progress, handling large file uploads, or sending
7
+ mixed form data with files.
8
+ ---
9
+
10
+ # Multipart Upload
11
+
12
+ ## Overview
13
+
14
+ Multipart upload sends files and form data in a single HTTP request using `multipart/form-data` encoding. On Android, files are typically picked from the gallery or camera, converted to a `MultipartBody.Part`, and sent via Retrofit or Ktor. Progress tracking requires a custom `RequestBody` wrapper.
15
+
16
+ ---
17
+
18
+ ## Core Principles
19
+
20
+ - Never read the full file into memory — stream from `Uri` using `ContentResolver`
21
+ - Track upload progress via a custom `RequestBody` wrapper — not polling
22
+ - Run uploads in a `CoroutineWorker` for long-running or background uploads
23
+ - Compress images before upload when size matters — respect server limits
24
+ - Show progress in the UI via `StateFlow` — cancel is a first-class operation
25
+
26
+ ---
27
+
28
+ ## Retrofit Multipart Upload
29
+
30
+ ```kotlin
31
+ // ✅ API interface
32
+ interface MediaApi {
33
+ @Multipart
34
+ @POST("users/{id}/avatar")
35
+ suspend fun uploadAvatar(
36
+ @Path("id") userId: String,
37
+ @Part avatar: MultipartBody.Part
38
+ ): AvatarDto
39
+
40
+ @Multipart
41
+ @POST("posts")
42
+ suspend fun createPost(
43
+ @Part("title") title: RequestBody,
44
+ @Part("description") description: RequestBody,
45
+ @Part image: MultipartBody.Part
46
+ ): PostDto
47
+ }
48
+
49
+ // ✅ Build MultipartBody.Part from Uri
50
+ fun Context.uriToMultipartPart(uri: Uri, partName: String): MultipartBody.Part {
51
+ val mimeType = contentResolver.getType(uri) ?: "application/octet-stream"
52
+ val fileName = uri.getFileName(contentResolver) ?: "upload"
53
+
54
+ val requestBody = object : RequestBody() {
55
+ override fun contentType() = mimeType.toMediaType()
56
+
57
+ override fun contentLength(): Long {
58
+ return contentResolver.openFileDescriptor(uri, "r")?.statSize ?: -1
59
+ }
60
+
61
+ override fun writeTo(sink: BufferedSink) {
62
+ contentResolver.openInputStream(uri)?.use { input ->
63
+ sink.writeAll(input.source())
64
+ }
65
+ }
66
+ }
67
+
68
+ return MultipartBody.Part.createFormData(partName, fileName, requestBody)
69
+ }
70
+
71
+ fun Uri.getFileName(contentResolver: ContentResolver): String? {
72
+ return contentResolver.query(this, null, null, null, null)?.use { cursor ->
73
+ val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
74
+ cursor.moveToFirst()
75
+ cursor.getString(index)
76
+ }
77
+ }
78
+ ```
79
+
80
+ ---
81
+
82
+ ## Progress Tracking
83
+
84
+ ```kotlin
85
+ // ✅ RequestBody wrapper with progress callback
86
+ class ProgressRequestBody(
87
+ private val delegate: RequestBody,
88
+ private val onProgress: (Int) -> Unit
89
+ ) : RequestBody() {
90
+
91
+ override fun contentType() = delegate.contentType()
92
+ override fun contentLength() = delegate.contentLength()
93
+
94
+ override fun writeTo(sink: BufferedSink) {
95
+ val total = contentLength()
96
+ var uploaded = 0L
97
+
98
+ val progressSink = object : ForwardingSink(sink) {
99
+ override fun write(source: Buffer, byteCount: Long) {
100
+ super.write(source, byteCount)
101
+ uploaded += byteCount
102
+ val progress = if (total > 0) ((uploaded * 100) / total).toInt() else 0
103
+ onProgress(progress)
104
+ }
105
+ }
106
+
107
+ delegate.writeTo(progressSink.buffer())
108
+ }
109
+ }
110
+
111
+ // ✅ Use in ViewModel
112
+ class UploadViewModel @Inject constructor(
113
+ private val mediaRepository: MediaRepository
114
+ ) : ViewModel() {
115
+
116
+ private val _progress = MutableStateFlow(0)
117
+ val progress: StateFlow<Int> = _progress.asStateFlow()
118
+
119
+ private val _state = MutableStateFlow<UploadState>(UploadState.Idle)
120
+ val state: StateFlow<UploadState> = _state.asStateFlow()
121
+
122
+ private var uploadJob: Job? = null
123
+
124
+ fun upload(uri: Uri) {
125
+ uploadJob = viewModelScope.launch {
126
+ _state.value = UploadState.Uploading
127
+ mediaRepository.uploadFile(uri) { progress ->
128
+ _progress.value = progress
129
+ }.fold(
130
+ onSuccess = { _state.value = UploadState.Success(it) },
131
+ onFailure = { _state.value = UploadState.Error(it.message ?: "Upload failed") }
132
+ )
133
+ }
134
+ }
135
+
136
+ fun cancel() {
137
+ uploadJob?.cancel()
138
+ _state.value = UploadState.Idle
139
+ _progress.value = 0
140
+ }
141
+ }
142
+
143
+ sealed interface UploadState {
144
+ data object Idle : UploadState
145
+ data object Uploading : UploadState
146
+ data class Success(val url: String) : UploadState
147
+ data class Error(val message: String) : UploadState
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## Image Compression Before Upload
154
+
155
+ ```kotlin
156
+ // ✅ Compress bitmap before upload
157
+ fun Uri.compressToByteArray(
158
+ context: Context,
159
+ maxWidth: Int = 1024,
160
+ maxHeight: Int = 1024,
161
+ quality: Int = 85
162
+ ): ByteArray {
163
+ val bitmap = BitmapFactory.decodeStream(context.contentResolver.openInputStream(this))
164
+ val scaled = Bitmap.createScaledBitmap(
165
+ bitmap,
166
+ maxWidth.coerceAtMost(bitmap.width),
167
+ maxHeight.coerceAtMost(bitmap.height),
168
+ true
169
+ )
170
+ return ByteArrayOutputStream().also { out ->
171
+ scaled.compress(Bitmap.CompressFormat.JPEG, quality, out)
172
+ }.toByteArray()
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ## Ktor Multipart Upload
179
+
180
+ ```kotlin
181
+ // ✅ Ktor multipart upload
182
+ suspend fun uploadAvatar(userId: String, fileBytes: ByteArray, fileName: String): AvatarDto {
183
+ return client.submitFormWithBinaryData(
184
+ url = "users/$userId/avatar",
185
+ formData = formData {
186
+ append("avatar", fileBytes, Headers.build {
187
+ append(HttpHeaders.ContentType, "image/jpeg")
188
+ append(HttpHeaders.ContentDisposition, "filename=$fileName")
189
+ })
190
+ }
191
+ ).body()
192
+ }
193
+ ```
194
+
195
+ ---
196
+
197
+ ## Anti-Patterns
198
+
199
+ - Loading the entire file into a `ByteArray` before upload — causes OOM for large files
200
+ - Not showing progress for uploads over 1MB — user has no feedback
201
+ - No cancel mechanism — user is stuck waiting
202
+ - Not compressing images — wastes bandwidth and hits server size limits
203
+ - Blocking the main thread during file reading — use coroutines with IO dispatcher
204
+
205
+ ---
206
+
207
+ ## Related Skills
208
+
209
+ - `retrofit` — Retrofit client setup
210
+ - `ktor` — Ktor client for KMP
211
+ - `workmanager` — background upload for large or resumable uploads
212
+ - `camera` — capturing images to upload
213
+ - `filesystem` — reading files from device storage