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