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,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: okhttp
|
|
3
|
+
description: >
|
|
4
|
+
OkHttp client configuration and interceptor patterns for Android.
|
|
5
|
+
Load this skill when configuring OkHttp, writing custom interceptors,
|
|
6
|
+
setting up caching, managing timeouts, handling connection pooling,
|
|
7
|
+
or debugging HTTP traffic.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# OkHttp
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
OkHttp is the HTTP engine underneath Retrofit. It manages connection pooling, caching, interceptors, and TLS configuration. Correct OkHttp setup is critical for performance, security, and reliability.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- One `OkHttpClient` instance shared across all Retrofit instances — it manages the connection pool
|
|
20
|
+
- Use **interceptors** for cross-cutting concerns: auth, logging, retry, headers
|
|
21
|
+
- Configure **timeouts** explicitly — never rely on defaults in production
|
|
22
|
+
- Use `Cache` for GET responses to reduce bandwidth and improve offline resilience
|
|
23
|
+
- Never block the OkHttp thread inside an interceptor — use coroutines at the repository level
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Client Setup
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Full OkHttpClient configuration
|
|
31
|
+
@Provides
|
|
32
|
+
@Singleton
|
|
33
|
+
fun provideOkHttpClient(
|
|
34
|
+
authInterceptor: AuthInterceptor,
|
|
35
|
+
loggingInterceptor: HttpLoggingInterceptor,
|
|
36
|
+
cache: Cache
|
|
37
|
+
): OkHttpClient {
|
|
38
|
+
return OkHttpClient.Builder()
|
|
39
|
+
// Interceptors (order matters — application interceptors first)
|
|
40
|
+
.addInterceptor(authInterceptor)
|
|
41
|
+
.addInterceptor(loggingInterceptor)
|
|
42
|
+
// Timeouts
|
|
43
|
+
.connectTimeout(30, TimeUnit.SECONDS)
|
|
44
|
+
.readTimeout(30, TimeUnit.SECONDS)
|
|
45
|
+
.writeTimeout(30, TimeUnit.SECONDS)
|
|
46
|
+
.callTimeout(60, TimeUnit.SECONDS)
|
|
47
|
+
// Cache
|
|
48
|
+
.cache(cache)
|
|
49
|
+
// Connection pool
|
|
50
|
+
.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
|
|
51
|
+
.build()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Provides
|
|
55
|
+
@Singleton
|
|
56
|
+
fun provideCache(@ApplicationContext context: Context): Cache {
|
|
57
|
+
val cacheSize = 10L * 1024 * 1024 // 10 MB
|
|
58
|
+
return Cache(context.cacheDir, cacheSize)
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Interceptor Types
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Application Interceptors (addInterceptor)
|
|
68
|
+
→ Run before the request hits the network
|
|
69
|
+
→ See the original request and final response
|
|
70
|
+
→ Good for: auth headers, logging, retry
|
|
71
|
+
|
|
72
|
+
Network Interceptors (addNetworkInterceptor)
|
|
73
|
+
→ Run after redirects and cache decisions
|
|
74
|
+
→ See the actual network request/response
|
|
75
|
+
→ Good for: cache control headers, compression
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Custom Interceptors
|
|
81
|
+
|
|
82
|
+
```kotlin
|
|
83
|
+
// ✅ Header interceptor — add common headers to every request
|
|
84
|
+
class CommonHeadersInterceptor @Inject constructor(
|
|
85
|
+
private val appVersionProvider: AppVersionProvider
|
|
86
|
+
) : Interceptor {
|
|
87
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
88
|
+
val request = chain.request().newBuilder()
|
|
89
|
+
.addHeader("X-App-Version", appVersionProvider.version)
|
|
90
|
+
.addHeader("X-Platform", "android")
|
|
91
|
+
.addHeader("Accept-Language", Locale.getDefault().language)
|
|
92
|
+
.build()
|
|
93
|
+
return chain.proceed(request)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ✅ Token refresh interceptor — handle 401 by refreshing token
|
|
98
|
+
class TokenRefreshInterceptor @Inject constructor(
|
|
99
|
+
private val tokenRepository: TokenRepository
|
|
100
|
+
) : Interceptor {
|
|
101
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
102
|
+
val response = chain.proceed(chain.request())
|
|
103
|
+
|
|
104
|
+
if (response.code == 401) {
|
|
105
|
+
response.close()
|
|
106
|
+
val newToken = runBlocking { tokenRepository.refreshToken() }
|
|
107
|
+
if (newToken != null) {
|
|
108
|
+
val newRequest = chain.request().newBuilder()
|
|
109
|
+
.header("Authorization", "Bearer $newToken")
|
|
110
|
+
.build()
|
|
111
|
+
return chain.proceed(newRequest)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return response
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ✅ Cache control interceptor — force cache for specific endpoints
|
|
119
|
+
class CacheControlInterceptor : Interceptor {
|
|
120
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
121
|
+
val response = chain.proceed(chain.request())
|
|
122
|
+
return if (chain.request().url.pathSegments.contains("static")) {
|
|
123
|
+
response.newBuilder()
|
|
124
|
+
.header("Cache-Control", "public, max-age=86400") // 1 day
|
|
125
|
+
.build()
|
|
126
|
+
} else {
|
|
127
|
+
response
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Logging Interceptor
|
|
136
|
+
|
|
137
|
+
```kotlin
|
|
138
|
+
// ✅ Debug-only logging — never log in release
|
|
139
|
+
@Provides
|
|
140
|
+
@Singleton
|
|
141
|
+
fun provideLoggingInterceptor(): HttpLoggingInterceptor {
|
|
142
|
+
return HttpLoggingInterceptor { message ->
|
|
143
|
+
Timber.tag("OkHttp").d(message)
|
|
144
|
+
}.apply {
|
|
145
|
+
level = if (BuildConfig.DEBUG)
|
|
146
|
+
HttpLoggingInterceptor.Level.BODY
|
|
147
|
+
else
|
|
148
|
+
HttpLoggingInterceptor.Level.NONE
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Response Extension
|
|
156
|
+
|
|
157
|
+
```kotlin
|
|
158
|
+
// ✅ Safe response body parsing
|
|
159
|
+
fun Response.bodyOrThrow(): String {
|
|
160
|
+
return body?.string() ?: throw IOException("Empty response body")
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fun Response.isSuccess(): Boolean = code in 200..299
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Timeout Strategy
|
|
169
|
+
|
|
170
|
+
| Timeout | Recommended | Use Case |
|
|
171
|
+
|---------|-------------|----------|
|
|
172
|
+
| `connectTimeout` | 15–30s | Establishing TCP connection |
|
|
173
|
+
| `readTimeout` | 30–60s | Reading response body |
|
|
174
|
+
| `writeTimeout` | 30–60s | Sending request body |
|
|
175
|
+
| `callTimeout` | 60–120s | Entire call including redirects |
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Anti-Patterns
|
|
180
|
+
|
|
181
|
+
- Creating a new `OkHttpClient` per request — destroys connection pool benefits
|
|
182
|
+
- Using `runBlocking` inside interceptors for non-token-refresh logic — blocks threads
|
|
183
|
+
- Logging request bodies in release builds — security risk
|
|
184
|
+
- Setting `callTimeout` too low for file upload/download endpoints
|
|
185
|
+
- Adding too many interceptors in sequence that each read the response body — body can only be read once
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Related Skills
|
|
190
|
+
- `retrofit` — Retrofit setup on top of OkHttp
|
|
191
|
+
- `authentication` — token management and refresh flow
|
|
192
|
+
- `certificate-pinning` — TLS security via OkHttp
|
|
193
|
+
- `retry-backoff` — retry interceptor patterns
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rest
|
|
3
|
+
description: >
|
|
4
|
+
REST API design principles and consumption patterns for Android.
|
|
5
|
+
Load this skill when designing API contracts, structuring HTTP requests,
|
|
6
|
+
handling standard HTTP status codes, modeling API responses,
|
|
7
|
+
or understanding REST conventions used in the data layer.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# REST
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
REST (Representational State Transfer) is the standard architectural style for HTTP APIs. Understanding REST conventions ensures the data layer handles responses, errors, and status codes correctly and consistently.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use the correct HTTP method for each operation — GET, POST, PUT, PATCH, DELETE
|
|
20
|
+
- Map HTTP status codes to domain errors — never ignore non-2xx responses
|
|
21
|
+
- Use query parameters for filtering/sorting/pagination — path parameters for resource identity
|
|
22
|
+
- Requests and responses use consistent naming — snake_case in JSON, camelCase in Kotlin DTOs
|
|
23
|
+
- Idempotent operations (GET, PUT, DELETE) are safe to retry — non-idempotent (POST) are not
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## HTTP Method Mapping
|
|
28
|
+
|
|
29
|
+
| Method | Use For | Idempotent |
|
|
30
|
+
|--------|---------|------------|
|
|
31
|
+
| `GET` | Fetch resource(s) | ✅ Yes |
|
|
32
|
+
| `POST` | Create new resource | ❌ No |
|
|
33
|
+
| `PUT` | Replace entire resource | ✅ Yes |
|
|
34
|
+
| `PATCH` | Partial update | ❌ No |
|
|
35
|
+
| `DELETE` | Remove resource | ✅ Yes |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Standard Endpoints Pattern
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
GET /users → list users
|
|
43
|
+
POST /users → create user
|
|
44
|
+
GET /users/{id} → get user by id
|
|
45
|
+
PUT /users/{id} → replace user
|
|
46
|
+
PATCH /users/{id} → partial update user
|
|
47
|
+
DELETE /users/{id} → delete user
|
|
48
|
+
|
|
49
|
+
GET /users/{id}/posts → posts belonging to user
|
|
50
|
+
POST /users/{id}/posts → create post for user
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## HTTP Status Code Handling
|
|
56
|
+
|
|
57
|
+
```kotlin
|
|
58
|
+
// ✅ Map status codes to domain errors
|
|
59
|
+
sealed class ApiError : Exception() {
|
|
60
|
+
data class BadRequest(override val message: String) : ApiError() // 400
|
|
61
|
+
data class Unauthorized(override val message: String) : ApiError() // 401
|
|
62
|
+
data class Forbidden(override val message: String) : ApiError() // 403
|
|
63
|
+
data class NotFound(override val message: String) : ApiError() // 404
|
|
64
|
+
data class Conflict(override val message: String) : ApiError() // 409
|
|
65
|
+
data class UnprocessableEntity(val errors: Map<String, List<String>>) : ApiError() // 422
|
|
66
|
+
data class TooManyRequests(val retryAfter: Int?) : ApiError() // 429
|
|
67
|
+
data class ServerError(val code: Int) : ApiError() // 5xx
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ✅ Map HttpException to ApiError
|
|
71
|
+
fun HttpException.toApiError(): ApiError {
|
|
72
|
+
return when (code()) {
|
|
73
|
+
400 -> ApiError.BadRequest(parseErrorMessage())
|
|
74
|
+
401 -> ApiError.Unauthorized(parseErrorMessage())
|
|
75
|
+
403 -> ApiError.Forbidden(parseErrorMessage())
|
|
76
|
+
404 -> ApiError.NotFound(parseErrorMessage())
|
|
77
|
+
409 -> ApiError.Conflict(parseErrorMessage())
|
|
78
|
+
422 -> ApiError.UnprocessableEntity(parseValidationErrors())
|
|
79
|
+
429 -> ApiError.TooManyRequests(response()?.headers()?.get("Retry-After")?.toIntOrNull())
|
|
80
|
+
in 500..599 -> ApiError.ServerError(code())
|
|
81
|
+
else -> ApiError.ServerError(code())
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Pagination Patterns
|
|
89
|
+
|
|
90
|
+
```kotlin
|
|
91
|
+
// ✅ Offset-based pagination
|
|
92
|
+
@GET("users")
|
|
93
|
+
suspend fun getUsers(
|
|
94
|
+
@Query("page") page: Int,
|
|
95
|
+
@Query("limit") limit: Int = 20
|
|
96
|
+
): PagedResponse<UserDto>
|
|
97
|
+
|
|
98
|
+
@Serializable
|
|
99
|
+
data class PagedResponse<T>(
|
|
100
|
+
val data: List<T>,
|
|
101
|
+
val total: Int,
|
|
102
|
+
val page: Int,
|
|
103
|
+
val limit: Int,
|
|
104
|
+
val hasMore: Boolean
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// ✅ Cursor-based pagination
|
|
108
|
+
@GET("feed")
|
|
109
|
+
suspend fun getFeed(
|
|
110
|
+
@Query("cursor") cursor: String? = null,
|
|
111
|
+
@Query("limit") limit: Int = 20
|
|
112
|
+
): CursorPagedResponse<FeedItemDto>
|
|
113
|
+
|
|
114
|
+
@Serializable
|
|
115
|
+
data class CursorPagedResponse<T>(
|
|
116
|
+
val data: List<T>,
|
|
117
|
+
val nextCursor: String?,
|
|
118
|
+
val hasMore: Boolean
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Standard Response Envelope
|
|
125
|
+
|
|
126
|
+
```kotlin
|
|
127
|
+
// ✅ Consistent API envelope — if the API uses one
|
|
128
|
+
@Serializable
|
|
129
|
+
data class ApiResponse<T>(
|
|
130
|
+
val success: Boolean,
|
|
131
|
+
val data: T? = null,
|
|
132
|
+
val error: ApiErrorDto? = null,
|
|
133
|
+
val message: String? = null
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
@Serializable
|
|
137
|
+
data class ApiErrorDto(
|
|
138
|
+
val code: String,
|
|
139
|
+
val message: String,
|
|
140
|
+
val details: Map<String, List<String>> = emptyMap()
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Query Parameter Conventions
|
|
147
|
+
|
|
148
|
+
```kotlin
|
|
149
|
+
// ✅ Common query parameter patterns
|
|
150
|
+
@GET("users")
|
|
151
|
+
suspend fun getUsers(
|
|
152
|
+
@Query("q") search: String? = null, // search
|
|
153
|
+
@Query("sort") sort: String? = "created_at", // sort field
|
|
154
|
+
@Query("order") order: String? = "desc", // sort direction
|
|
155
|
+
@Query("filter[status]") status: String? = null, // filtering
|
|
156
|
+
@Query("page") page: Int = 1,
|
|
157
|
+
@Query("limit") limit: Int = 20
|
|
158
|
+
): PagedResponse<UserDto>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Anti-Patterns
|
|
164
|
+
|
|
165
|
+
- Using `GET` for operations with side effects — use `POST` or `DELETE`
|
|
166
|
+
- Ignoring HTTP status codes — check response codes, don't assume 200
|
|
167
|
+
- Putting sensitive data in query parameters — use request body or headers
|
|
168
|
+
- Using `POST` for reads — breaks caching and idempotency
|
|
169
|
+
- Returning `200 OK` for errors with error details in the body — use correct status codes
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Related Skills
|
|
174
|
+
- `retrofit` — implementing REST calls with Retrofit
|
|
175
|
+
- `api-contract` — formal API contract definition
|
|
176
|
+
- `authentication` — auth headers and token handling
|
|
177
|
+
- `retry-backoff` — safe retry for idempotent requests
|
|
178
|
+
- `dto-mapping` — mapping REST responses to domain models
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rate-limiting
|
|
3
|
+
description: >
|
|
4
|
+
Handling rate limiting (HTTP 429) and implementing client-side request
|
|
5
|
+
throttling in Android. Load this skill when handling 429 Too Many Requests
|
|
6
|
+
responses, respecting Retry-After headers, implementing request queuing,
|
|
7
|
+
or throttling outgoing requests to avoid hitting server limits.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Rate Limiting
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Rate limiting occurs when the server rejects requests because the client has exceeded the allowed request frequency. On Android, this is handled in two ways: reacting to 429 responses from the server, and proactively throttling client-side requests to avoid hitting the limit.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Always respect the `Retry-After` header when present — don't guess the delay
|
|
20
|
+
- Implement **client-side throttling** for high-frequency operations (search, autocomplete)
|
|
21
|
+
- Use a **single retry** for 429 — not an infinite loop
|
|
22
|
+
- Log rate limit events — they indicate either a bug or a need to adjust request frequency
|
|
23
|
+
- Debounce user-driven requests (search input) before they reach the network
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Handling 429 Server Response
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ 429 handler in repository
|
|
31
|
+
suspend fun <T> withRateLimitHandling(block: suspend () -> T): Result<T> {
|
|
32
|
+
return runCatching { block() }
|
|
33
|
+
.recoverCatching { throwable ->
|
|
34
|
+
if (throwable is HttpException && throwable.code() == 429) {
|
|
35
|
+
val retryAfterSeconds = throwable.response()
|
|
36
|
+
?.headers()
|
|
37
|
+
?.get("Retry-After")
|
|
38
|
+
?.toLongOrNull()
|
|
39
|
+
?: 10L // default 10 seconds if header missing
|
|
40
|
+
|
|
41
|
+
delay(retryAfterSeconds * 1_000)
|
|
42
|
+
block() // single retry after waiting
|
|
43
|
+
} else {
|
|
44
|
+
throw throwable
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ✅ Usage
|
|
50
|
+
override suspend fun searchUsers(query: String): Result<List<User>> =
|
|
51
|
+
withRateLimitHandling {
|
|
52
|
+
api.searchUsers(query).map { mapper.toDomain(it) }
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## OkHttp Rate Limit Interceptor
|
|
59
|
+
|
|
60
|
+
```kotlin
|
|
61
|
+
// ✅ Intercept 429 at the HTTP level
|
|
62
|
+
class RateLimitInterceptor : Interceptor {
|
|
63
|
+
override fun intercept(chain: Interceptor.Chain): Response {
|
|
64
|
+
val response = chain.proceed(chain.request())
|
|
65
|
+
|
|
66
|
+
if (response.code == 429) {
|
|
67
|
+
val retryAfter = response.headers["Retry-After"]?.toLongOrNull() ?: 10L
|
|
68
|
+
response.close()
|
|
69
|
+
Thread.sleep(retryAfter * 1_000)
|
|
70
|
+
return chain.proceed(chain.request())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return response
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Client-Side Debouncing (Search / Autocomplete)
|
|
81
|
+
|
|
82
|
+
```kotlin
|
|
83
|
+
// ✅ Debounce search input in ViewModel — reduce API calls
|
|
84
|
+
class SearchViewModel @Inject constructor(
|
|
85
|
+
private val searchUseCase: SearchUsersUseCase
|
|
86
|
+
) : ViewModel() {
|
|
87
|
+
|
|
88
|
+
private val _query = MutableStateFlow("")
|
|
89
|
+
private val _results = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
|
|
90
|
+
val results: StateFlow<UiState<List<User>>> = _results.asStateFlow()
|
|
91
|
+
|
|
92
|
+
init {
|
|
93
|
+
viewModelScope.launch {
|
|
94
|
+
_query
|
|
95
|
+
.debounce(300) // ✅ wait 300ms after last keystroke
|
|
96
|
+
.filter { it.length >= 2 } // ✅ minimum query length
|
|
97
|
+
.distinctUntilChanged() // ✅ skip duplicate queries
|
|
98
|
+
.collectLatest { query -> // ✅ cancel previous in-flight request
|
|
99
|
+
_results.value = UiState.Loading
|
|
100
|
+
searchUseCase(query).fold(
|
|
101
|
+
onSuccess = { _results.value = UiState.Success(it) },
|
|
102
|
+
onFailure = { _results.value = UiState.Error(it.message ?: "Error") }
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fun onQueryChange(query: String) {
|
|
109
|
+
_query.value = query
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Request Throttler (Token Bucket)
|
|
117
|
+
|
|
118
|
+
```kotlin
|
|
119
|
+
// ✅ Token bucket throttler — max N requests per window
|
|
120
|
+
class RequestThrottler(
|
|
121
|
+
private val maxRequests: Int = 10,
|
|
122
|
+
private val windowMs: Long = 1_000L
|
|
123
|
+
) {
|
|
124
|
+
private val mutex = Mutex()
|
|
125
|
+
private val timestamps = ArrayDeque<Long>()
|
|
126
|
+
|
|
127
|
+
suspend fun throttle() {
|
|
128
|
+
mutex.withLock {
|
|
129
|
+
val now = System.currentTimeMillis()
|
|
130
|
+
// remove timestamps outside the window
|
|
131
|
+
while (timestamps.isNotEmpty() && now - timestamps.first() > windowMs) {
|
|
132
|
+
timestamps.removeFirst()
|
|
133
|
+
}
|
|
134
|
+
if (timestamps.size >= maxRequests) {
|
|
135
|
+
val waitMs = windowMs - (now - timestamps.first())
|
|
136
|
+
delay(waitMs)
|
|
137
|
+
}
|
|
138
|
+
timestamps.addLast(System.currentTimeMillis())
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ✅ Usage
|
|
144
|
+
class AnalyticsDataSource @Inject constructor(
|
|
145
|
+
private val throttler: RequestThrottler
|
|
146
|
+
) {
|
|
147
|
+
suspend fun trackEvent(event: AnalyticsEvent) {
|
|
148
|
+
throttler.throttle()
|
|
149
|
+
api.track(event)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Anti-Patterns
|
|
157
|
+
|
|
158
|
+
- Retrying 429 immediately without delay — will get another 429 immediately
|
|
159
|
+
- Ignoring the `Retry-After` header — use the server-specified delay
|
|
160
|
+
- Not debouncing search input — fires a request on every keystroke
|
|
161
|
+
- Infinite retry loop on 429 — cap at one retry after the wait
|
|
162
|
+
- Using `Thread.sleep` in coroutine context — use `delay` instead
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Related Skills
|
|
167
|
+
- `retry-backoff` — general retry strategy for transient failures
|
|
168
|
+
- `okhttp` — interceptor setup for HTTP-level handling
|
|
169
|
+
- `flow` — debounce and distinctUntilChanged operators
|
|
170
|
+
- `fallback-strategy` — what to do when rate limit retries fail
|