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,241 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: retrofit
|
|
3
|
+
description: >
|
|
4
|
+
Retrofit HTTP client setup and usage for Android.
|
|
5
|
+
Load this skill when setting up Retrofit, defining API interfaces,
|
|
6
|
+
configuring converters, adding interceptors, handling responses,
|
|
7
|
+
or integrating Retrofit with Kotlinx Serialization and OkHttp.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Retrofit
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
Retrofit is a type-safe HTTP client for Android that turns API interfaces into callable Kotlin functions. It integrates with OkHttp for the underlying HTTP layer and Kotlinx Serialization for JSON parsing.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Core Principles
|
|
19
|
+
|
|
20
|
+
- Define one `Retrofit` instance per base URL — use DI to provide it
|
|
21
|
+
- API interfaces return `Result<T>` or a sealed type — never raw response objects
|
|
22
|
+
- Never call Retrofit from UI or ViewModel directly — go through Repository
|
|
23
|
+
- Use `suspend` functions for all API calls — no callbacks or `Call<T>`
|
|
24
|
+
- Handle HTTP errors explicitly — don't let them surface as exceptions silently
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Setup
|
|
29
|
+
|
|
30
|
+
```toml
|
|
31
|
+
# libs.versions.toml
|
|
32
|
+
[versions]
|
|
33
|
+
retrofit = "2.11.0"
|
|
34
|
+
okhttp = "4.12.0"
|
|
35
|
+
|
|
36
|
+
[libraries]
|
|
37
|
+
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
|
38
|
+
retrofit-kotlinx-serialization = { module = "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter", version.ref = "retrofit" }
|
|
39
|
+
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
|
40
|
+
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
```kotlin
|
|
44
|
+
// build.gradle.kts
|
|
45
|
+
dependencies {
|
|
46
|
+
implementation(libs.retrofit)
|
|
47
|
+
implementation(libs.retrofit.kotlinx.serialization)
|
|
48
|
+
implementation(libs.okhttp)
|
|
49
|
+
implementation(libs.okhttp.logging)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Retrofit Instance
|
|
56
|
+
|
|
57
|
+
```kotlin
|
|
58
|
+
// ✅ Single Retrofit instance provided via DI
|
|
59
|
+
@Provides
|
|
60
|
+
@Singleton
|
|
61
|
+
fun provideRetrofit(okHttpClient: OkHttpClient, json: Json): Retrofit {
|
|
62
|
+
return Retrofit.Builder()
|
|
63
|
+
.baseUrl(BuildConfig.BASE_URL)
|
|
64
|
+
.client(okHttpClient)
|
|
65
|
+
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
|
66
|
+
.build()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Provides
|
|
70
|
+
@Singleton
|
|
71
|
+
fun provideOkHttpClient(
|
|
72
|
+
authInterceptor: AuthInterceptor,
|
|
73
|
+
loggingInterceptor: HttpLoggingInterceptor
|
|
74
|
+
): OkHttpClient {
|
|
75
|
+
return OkHttpClient.Builder()
|
|
76
|
+
.addInterceptor(authInterceptor)
|
|
77
|
+
.addInterceptor(loggingInterceptor)
|
|
78
|
+
.connectTimeout(30, TimeUnit.SECONDS)
|
|
79
|
+
.readTimeout(30, TimeUnit.SECONDS)
|
|
80
|
+
.writeTimeout(30, TimeUnit.SECONDS)
|
|
81
|
+
.build()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@Provides
|
|
85
|
+
@Singleton
|
|
86
|
+
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
|
|
87
|
+
return HttpLoggingInterceptor().apply {
|
|
88
|
+
level = if (BuildConfig.DEBUG)
|
|
89
|
+
HttpLoggingInterceptor.Level.BODY
|
|
90
|
+
else
|
|
91
|
+
HttpLoggingInterceptor.Level.NONE
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## API Interface
|
|
99
|
+
|
|
100
|
+
```kotlin
|
|
101
|
+
// ✅ Suspend functions, typed responses
|
|
102
|
+
interface UserApi {
|
|
103
|
+
|
|
104
|
+
@GET("users")
|
|
105
|
+
suspend fun getUsers(): List<UserDto>
|
|
106
|
+
|
|
107
|
+
@GET("users/{id}")
|
|
108
|
+
suspend fun getUser(@Path("id") id: String): UserDto
|
|
109
|
+
|
|
110
|
+
@POST("users")
|
|
111
|
+
suspend fun createUser(@Body body: CreateUserRequest): UserDto
|
|
112
|
+
|
|
113
|
+
@PUT("users/{id}")
|
|
114
|
+
suspend fun updateUser(
|
|
115
|
+
@Path("id") id: String,
|
|
116
|
+
@Body body: UpdateUserRequest
|
|
117
|
+
): UserDto
|
|
118
|
+
|
|
119
|
+
@DELETE("users/{id}")
|
|
120
|
+
suspend fun deleteUser(@Path("id") id: String)
|
|
121
|
+
|
|
122
|
+
@GET("users")
|
|
123
|
+
suspend fun searchUsers(
|
|
124
|
+
@Query("q") query: String,
|
|
125
|
+
@Query("page") page: Int = 1,
|
|
126
|
+
@Query("limit") limit: Int = 20
|
|
127
|
+
): PagedResponse<UserDto>
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Response Handling in Repository
|
|
134
|
+
|
|
135
|
+
```kotlin
|
|
136
|
+
// ✅ Wrap API calls in runCatching — map to domain Result
|
|
137
|
+
class UserRepositoryImpl @Inject constructor(
|
|
138
|
+
private val api: UserApi,
|
|
139
|
+
private val mapper: UserMapper
|
|
140
|
+
) : UserRepository {
|
|
141
|
+
|
|
142
|
+
override suspend fun getUser(id: String): Result<User> = runCatching {
|
|
143
|
+
val dto = api.getUser(id)
|
|
144
|
+
mapper.toDomain(dto)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override suspend fun getUsers(): Result<List<User>> = runCatching {
|
|
148
|
+
api.getUsers().map { mapper.toDomain(it) }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Auth Interceptor
|
|
156
|
+
|
|
157
|
+
```kotlin
|
|
158
|
+
// ✅ Attach token via interceptor — not in each API call
|
|
159
|
+
class AuthInterceptor @Inject constructor(
|
|
160
|
+
private val tokenProvider: TokenProvider
|
|
161
|
+
) : Interceptor {
|
|
162
|
+
|
|
163
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
164
|
+
val token = tokenProvider.getToken()
|
|
165
|
+
val request = if (token != null) {
|
|
166
|
+
chain.request().newBuilder()
|
|
167
|
+
.addHeader("Authorization", "Bearer $token")
|
|
168
|
+
.build()
|
|
169
|
+
} else {
|
|
170
|
+
chain.request()
|
|
171
|
+
}
|
|
172
|
+
return chain.proceed(request)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## HTTP Error Handling
|
|
180
|
+
|
|
181
|
+
```kotlin
|
|
182
|
+
// ✅ Custom call adapter or extension for HTTP errors
|
|
183
|
+
suspend fun <T> safeApiCall(call: suspend () -> T): Result<T> = runCatching {
|
|
184
|
+
call()
|
|
185
|
+
}.recoverCatching { throwable ->
|
|
186
|
+
when (throwable) {
|
|
187
|
+
is HttpException -> throw ApiException(
|
|
188
|
+
code = throwable.code(),
|
|
189
|
+
message = throwable.response()?.errorBody()?.string() ?: "HTTP error"
|
|
190
|
+
)
|
|
191
|
+
is IOException -> throw NetworkException("No internet connection")
|
|
192
|
+
else -> throw throwable
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ✅ Usage in repository
|
|
197
|
+
override suspend fun getUser(id: String): Result<User> =
|
|
198
|
+
safeApiCall { api.getUser(id) }
|
|
199
|
+
.map { mapper.toDomain(it) }
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Multipart / File Upload
|
|
205
|
+
|
|
206
|
+
```kotlin
|
|
207
|
+
// ✅ Upload file with multipart
|
|
208
|
+
@Multipart
|
|
209
|
+
@POST("users/{id}/avatar")
|
|
210
|
+
suspend fun uploadAvatar(
|
|
211
|
+
@Path("id") id: String,
|
|
212
|
+
@Part avatar: MultipartBody.Part
|
|
213
|
+
): AvatarDto
|
|
214
|
+
|
|
215
|
+
// ✅ Build MultipartBody.Part from file
|
|
216
|
+
fun File.toMultipartPart(partName: String): MultipartBody.Part {
|
|
217
|
+
val requestBody = asRequestBody("image/*".toMediaType())
|
|
218
|
+
return MultipartBody.Part.createFormData(partName, name, requestBody)
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Anti-Patterns
|
|
225
|
+
|
|
226
|
+
- Creating Retrofit instance per API call — expensive, use singleton
|
|
227
|
+
- Returning `Response<T>` from API interface — wrap in `Result` at the repository level
|
|
228
|
+
- Using `Call<T>` instead of `suspend` — unnecessary callback complexity
|
|
229
|
+
- Adding auth token manually in each API function — use interceptor
|
|
230
|
+
- Catching exceptions in ViewModel — handle in repository or use case
|
|
231
|
+
- Using `Gson` converter — use Kotlinx Serialization
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Related Skills
|
|
236
|
+
|
|
237
|
+
- `okhttp` — OkHttp client, interceptors, and connection config
|
|
238
|
+
- `serialization` — JSON parsing with Kotlinx Serialization
|
|
239
|
+
- `authentication` — token management and refresh
|
|
240
|
+
- `retry-backoff` — retry logic for failed requests
|
|
241
|
+
- `dto-mapping` — mapping API responses to domain models
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: retry-backoff
|
|
3
|
+
description: >
|
|
4
|
+
Retry and exponential backoff strategies for network requests in Android.
|
|
5
|
+
Load this skill when implementing automatic retry for failed requests,
|
|
6
|
+
configuring backoff intervals, handling idempotency, or building
|
|
7
|
+
resilient network layers.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Retry / Backoff
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Retry with exponential backoff improves resilience against transient network failures and temporary server errors. The key constraint is that only **idempotent** requests (GET, PUT, DELETE) should be retried automatically — POST requests must not be retried without explicit idempotency tokens.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Only retry **idempotent** operations automatically — never POST without idempotency token
|
|
20
|
+
- Use **exponential backoff** with jitter — prevents thundering herd on server recovery
|
|
21
|
+
- Set a **maximum retry count** — don't retry indefinitely
|
|
22
|
+
- Do not retry **4xx errors** (except 429) — they indicate a client error, not transient failure
|
|
23
|
+
- Retry **5xx errors** and network errors (`IOException`) — these are transient
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Retryable Conditions
|
|
28
|
+
|
|
29
|
+
| Condition | Retry? |
|
|
30
|
+
|-----------|--------|
|
|
31
|
+
| `IOException` (no internet) | ✅ Yes |
|
|
32
|
+
| `500` Server Error | ✅ Yes |
|
|
33
|
+
| `502` Bad Gateway | ✅ Yes |
|
|
34
|
+
| `503` Service Unavailable | ✅ Yes |
|
|
35
|
+
| `429` Too Many Requests | ✅ Yes (after Retry-After) |
|
|
36
|
+
| `401` Unauthorized | ❌ No (handle via token refresh) |
|
|
37
|
+
| `404` Not Found | ❌ No |
|
|
38
|
+
| `400` Bad Request | ❌ No |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Coroutine Retry Extension
|
|
43
|
+
|
|
44
|
+
```kotlin
|
|
45
|
+
// ✅ Generic retry with exponential backoff
|
|
46
|
+
suspend fun <T> withRetry(
|
|
47
|
+
maxAttempts: Int = 3,
|
|
48
|
+
initialDelay: Long = 500L,
|
|
49
|
+
maxDelay: Long = 10_000L,
|
|
50
|
+
factor: Double = 2.0,
|
|
51
|
+
shouldRetry: (Throwable) -> Boolean = ::isRetryable,
|
|
52
|
+
block: suspend () -> T
|
|
53
|
+
): T {
|
|
54
|
+
var currentDelay = initialDelay
|
|
55
|
+
repeat(maxAttempts - 1) { attempt ->
|
|
56
|
+
runCatching { block() }
|
|
57
|
+
.onSuccess { return it }
|
|
58
|
+
.onFailure { throwable ->
|
|
59
|
+
if (!shouldRetry(throwable)) throw throwable
|
|
60
|
+
val jitter = (0..200).random()
|
|
61
|
+
delay(currentDelay + jitter)
|
|
62
|
+
currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return block() // last attempt — let it throw
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun isRetryable(throwable: Throwable): Boolean {
|
|
69
|
+
return when (throwable) {
|
|
70
|
+
is IOException -> true
|
|
71
|
+
is HttpException -> throwable.code() in listOf(500, 502, 503, 504)
|
|
72
|
+
else -> false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Usage in Repository
|
|
80
|
+
|
|
81
|
+
```kotlin
|
|
82
|
+
// ✅ Wrap idempotent calls with retry
|
|
83
|
+
override suspend fun getUser(id: String): Result<User> = runCatching {
|
|
84
|
+
withRetry(maxAttempts = 3) {
|
|
85
|
+
api.getUser(id)
|
|
86
|
+
}.let { mapper.toDomain(it) }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ✅ Non-idempotent — no automatic retry
|
|
90
|
+
override suspend fun createUser(user: User): Result<User> = runCatching {
|
|
91
|
+
val dto = api.createUser(mapper.toRequest(user)) // no withRetry
|
|
92
|
+
mapper.toDomain(dto)
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## OkHttp Retry Interceptor
|
|
99
|
+
|
|
100
|
+
```kotlin
|
|
101
|
+
// ✅ Network-level retry for transient failures
|
|
102
|
+
class RetryInterceptor(
|
|
103
|
+
private val maxRetries: Int = 3
|
|
104
|
+
) : Interceptor {
|
|
105
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
106
|
+
val request = chain.request()
|
|
107
|
+
|
|
108
|
+
// Only retry idempotent methods
|
|
109
|
+
if (request.method !in listOf("GET", "PUT", "DELETE", "HEAD")) {
|
|
110
|
+
return chain.proceed(request)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var attempt = 0
|
|
114
|
+
var lastException: IOException? = null
|
|
115
|
+
|
|
116
|
+
while (attempt < maxRetries) {
|
|
117
|
+
try {
|
|
118
|
+
val response = chain.proceed(request)
|
|
119
|
+
if (response.isSuccessful || response.code !in listOf(500, 502, 503)) {
|
|
120
|
+
return response
|
|
121
|
+
}
|
|
122
|
+
response.close()
|
|
123
|
+
} catch (e: IOException) {
|
|
124
|
+
lastException = e
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
attempt++
|
|
128
|
+
if (attempt < maxRetries) {
|
|
129
|
+
val delay = (500L * 2.0.pow(attempt)).toLong().coerceAtMost(10_000L)
|
|
130
|
+
Thread.sleep(delay)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
throw lastException ?: IOException("Max retries exceeded")
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 429 Too Many Requests Handling
|
|
142
|
+
|
|
143
|
+
```kotlin
|
|
144
|
+
// ✅ Respect Retry-After header on 429
|
|
145
|
+
suspend fun <T> withRateLimitRetry(block: suspend () -> T): T {
|
|
146
|
+
while (true) {
|
|
147
|
+
val result = runCatching { block() }
|
|
148
|
+
result.onSuccess { return it }
|
|
149
|
+
result.onFailure { throwable ->
|
|
150
|
+
if (throwable is HttpException && throwable.code() == 429) {
|
|
151
|
+
val retryAfter = throwable.response()
|
|
152
|
+
?.headers()
|
|
153
|
+
?.get("Retry-After")
|
|
154
|
+
?.toLongOrNull()
|
|
155
|
+
?: 5L
|
|
156
|
+
delay(retryAfter * 1000)
|
|
157
|
+
} else {
|
|
158
|
+
throw throwable
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Anti-Patterns
|
|
168
|
+
|
|
169
|
+
- Retrying POST requests without idempotency keys — creates duplicate resources
|
|
170
|
+
- Retrying 4xx errors — client errors won't resolve on retry
|
|
171
|
+
- Fixed delay between retries — causes thundering herd; use backoff + jitter
|
|
172
|
+
- Infinite retries — always cap with `maxAttempts`
|
|
173
|
+
- Retrying inside the ViewModel — retry belongs in the repository or data source layer
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Related Skills
|
|
178
|
+
- `okhttp` — network interceptor for retry at the HTTP level
|
|
179
|
+
- `retrofit` — repository-level error handling
|
|
180
|
+
- `fallback-strategy` — what to do after all retries are exhausted
|
|
181
|
+
- `rate-limiting` — handling 429 responses
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: server-sent-events
|
|
3
|
+
description: >
|
|
4
|
+
Server-Sent Events (SSE) implementation in Android.
|
|
5
|
+
Load this skill when consuming a one-way real-time event stream from
|
|
6
|
+
the server, integrating SSE with AI streaming responses, handling
|
|
7
|
+
reconnection, or modeling SSE as a Flow.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Server-Sent Events (SSE)
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Server-Sent Events (SSE) is a one-way, server-to-client streaming protocol over HTTP. Unlike WebSockets, SSE is unidirectional and uses standard HTTP — making it simpler and more firewall-friendly. It is commonly used for AI streaming responses (LLMs), live notifications, and real-time dashboards.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- SSE is one-way — use WebSocket if bidirectional communication is needed
|
|
20
|
+
- Model the SSE stream as a `Flow` — integrates naturally with coroutines
|
|
21
|
+
- Handle reconnection — SSE streams drop on network changes
|
|
22
|
+
- Parse the `data:` field of each event — ignore comment lines (starting with `:`)
|
|
23
|
+
- Close the connection when the lifecycle owner is destroyed
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## OkHttp SSE Implementation
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ SSE client wrapping OkHttp EventSource
|
|
31
|
+
class SseClient @Inject constructor(
|
|
32
|
+
private val okHttpClient: OkHttpClient
|
|
33
|
+
) {
|
|
34
|
+
fun connect(url: String, headers: Map<String, String> = emptyMap()): Flow<SseEvent> =
|
|
35
|
+
callbackFlow {
|
|
36
|
+
val request = Request.Builder()
|
|
37
|
+
.url(url)
|
|
38
|
+
.apply { headers.forEach { (k, v) -> addHeader(k, v) } }
|
|
39
|
+
.addHeader("Accept", "text/event-stream")
|
|
40
|
+
.addHeader("Cache-Control", "no-cache")
|
|
41
|
+
.build()
|
|
42
|
+
|
|
43
|
+
val listener = object : EventSourceListener() {
|
|
44
|
+
override fun onEvent(
|
|
45
|
+
eventSource: EventSource,
|
|
46
|
+
id: String?,
|
|
47
|
+
type: String?,
|
|
48
|
+
data: String
|
|
49
|
+
) {
|
|
50
|
+
trySend(SseEvent.Data(id = id, type = type, data = data))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
override fun onFailure(
|
|
54
|
+
eventSource: EventSource,
|
|
55
|
+
t: Throwable?,
|
|
56
|
+
response: Response?
|
|
57
|
+
) {
|
|
58
|
+
close(t ?: IOException("SSE connection failed"))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override fun onClosed(eventSource: EventSource) {
|
|
62
|
+
trySend(SseEvent.Closed)
|
|
63
|
+
close()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
val eventSource = EventSources.createFactory(okHttpClient)
|
|
68
|
+
.newEventSource(request, listener)
|
|
69
|
+
|
|
70
|
+
awaitClose { eventSource.cancel() }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
sealed interface SseEvent {
|
|
75
|
+
data class Data(val id: String?, val type: String?, val data: String) : SseEvent
|
|
76
|
+
data object Closed : SseEvent
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Manual SSE Parsing (Without EventSource)
|
|
83
|
+
|
|
84
|
+
```kotlin
|
|
85
|
+
// ✅ Parse SSE stream manually from OkHttp response body
|
|
86
|
+
fun parseSSEStream(responseBody: ResponseBody): Flow<String> = flow {
|
|
87
|
+
responseBody.source().use { source ->
|
|
88
|
+
while (!source.exhausted()) {
|
|
89
|
+
val line = source.readUtf8Line() ?: break
|
|
90
|
+
when {
|
|
91
|
+
line.startsWith("data: ") -> emit(line.removePrefix("data: "))
|
|
92
|
+
line.startsWith(":") -> Unit // comment — ignore
|
|
93
|
+
line.isEmpty() -> Unit // event separator — ignore
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ✅ Usage with Retrofit streaming endpoint
|
|
100
|
+
@Streaming
|
|
101
|
+
@GET("chat/stream")
|
|
102
|
+
suspend fun streamChat(@Query("prompt") prompt: String): ResponseBody
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## AI Streaming Response
|
|
108
|
+
|
|
109
|
+
```kotlin
|
|
110
|
+
// ✅ Stream LLM token-by-token response
|
|
111
|
+
class ChatRepository @Inject constructor(
|
|
112
|
+
private val sseClient: SseClient,
|
|
113
|
+
private val tokenStorage: TokenStorage
|
|
114
|
+
) {
|
|
115
|
+
fun streamCompletion(prompt: String): Flow<String> {
|
|
116
|
+
return sseClient.connect(
|
|
117
|
+
url = "${BuildConfig.BASE_URL}chat/completions",
|
|
118
|
+
headers = mapOf(
|
|
119
|
+
"Authorization" to "Bearer ${tokenStorage.getAccessToken()}"
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
.filterIsInstance<SseEvent.Data>()
|
|
123
|
+
.filter { it.data != "[DONE]" }
|
|
124
|
+
.mapNotNull { event ->
|
|
125
|
+
runCatching {
|
|
126
|
+
json.decodeFromString<CompletionChunkDto>(event.data)
|
|
127
|
+
.choices.firstOrNull()?.delta?.content
|
|
128
|
+
}.getOrNull()
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ✅ Collect in ViewModel — append tokens to build response
|
|
134
|
+
class ChatViewModel @Inject constructor(
|
|
135
|
+
private val chatRepository: ChatRepository
|
|
136
|
+
) : ViewModel() {
|
|
137
|
+
|
|
138
|
+
private val _response = MutableStateFlow("")
|
|
139
|
+
val response: StateFlow<String> = _response.asStateFlow()
|
|
140
|
+
|
|
141
|
+
private val _isStreaming = MutableStateFlow(false)
|
|
142
|
+
val isStreaming: StateFlow<Boolean> = _isStreaming.asStateFlow()
|
|
143
|
+
|
|
144
|
+
fun sendMessage(prompt: String) {
|
|
145
|
+
viewModelScope.launch {
|
|
146
|
+
_response.value = ""
|
|
147
|
+
_isStreaming.value = true
|
|
148
|
+
chatRepository.streamCompletion(prompt)
|
|
149
|
+
.onCompletion { _isStreaming.value = false }
|
|
150
|
+
.catch { _isStreaming.value = false }
|
|
151
|
+
.collect { token ->
|
|
152
|
+
_response.value += token
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Reconnection
|
|
162
|
+
|
|
163
|
+
```kotlin
|
|
164
|
+
// ✅ Auto-reconnect on failure with backoff
|
|
165
|
+
fun connectWithReconnect(url: String, scope: CoroutineScope): Flow<SseEvent> = flow {
|
|
166
|
+
var attempt = 0
|
|
167
|
+
while (true) {
|
|
168
|
+
try {
|
|
169
|
+
emitAll(sseClient.connect(url))
|
|
170
|
+
attempt = 0 // reset on clean close
|
|
171
|
+
} catch (e: IOException) {
|
|
172
|
+
val delay = minOf(1_000L * (2.0.pow(attempt)).toLong(), 30_000L)
|
|
173
|
+
delay(delay)
|
|
174
|
+
attempt++
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}.flowOn(Dispatchers.IO)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Anti-Patterns
|
|
183
|
+
|
|
184
|
+
- Using SSE for bidirectional communication — use WebSocket instead
|
|
185
|
+
- Not closing the EventSource on lifecycle destroy — leaks the connection
|
|
186
|
+
- Accumulating all SSE events in memory — process and discard each event
|
|
187
|
+
- Not handling the `[DONE]` sentinel for AI streams — continues waiting indefinitely
|
|
188
|
+
- Blocking the main thread while reading the stream — always on IO dispatcher
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Related Skills
|
|
193
|
+
- `websocket` — bidirectional alternative to SSE
|
|
194
|
+
- `okhttp` — HTTP client for SSE connections
|
|
195
|
+
- `flow` — modeling streams with Kotlin Flow
|
|
196
|
+
- `retrofit` — streaming response body with `@Streaming`
|