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,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: websocket
|
|
3
|
+
description: >
|
|
4
|
+
WebSocket communication in Android using OkHttp or Ktor.
|
|
5
|
+
Load this skill when implementing real-time bidirectional communication,
|
|
6
|
+
managing WebSocket connection lifecycle, handling reconnection,
|
|
7
|
+
or processing incoming message streams as Flow.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# WebSocket
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
WebSockets provide full-duplex communication over a persistent TCP connection. On Android, OkHttp's `WebSocket` API is the standard for Retrofit-based projects, while Ktor provides native WebSocket support for KMP. Incoming messages are best modeled as a `Flow` to integrate naturally with the reactive architecture.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Model the WebSocket connection state explicitly — never assume it's always open
|
|
20
|
+
- Expose incoming messages as `Flow` — not callbacks
|
|
21
|
+
- Handle reconnection with exponential backoff — connections drop unexpectedly
|
|
22
|
+
- Close the WebSocket when the lifecycle owner is destroyed
|
|
23
|
+
- Never send messages when the connection is not in `OPEN` state
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Connection State Model
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
sealed interface WebSocketState {
|
|
31
|
+
data object Connecting : WebSocketState
|
|
32
|
+
data object Connected : WebSocketState
|
|
33
|
+
data class Message(val data: String) : WebSocketState
|
|
34
|
+
data class Error(val throwable: Throwable) : WebSocketState
|
|
35
|
+
data object Disconnected : WebSocketState
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## OkHttp WebSocket
|
|
42
|
+
|
|
43
|
+
```kotlin
|
|
44
|
+
// ✅ WebSocket manager wrapping OkHttp
|
|
45
|
+
class WebSocketManager @Inject constructor(
|
|
46
|
+
private val okHttpClient: OkHttpClient,
|
|
47
|
+
private val json: Json
|
|
48
|
+
) {
|
|
49
|
+
private var webSocket: WebSocket? = null
|
|
50
|
+
|
|
51
|
+
private val _state = MutableSharedFlow<WebSocketState>(
|
|
52
|
+
extraBufferCapacity = 64,
|
|
53
|
+
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
|
54
|
+
)
|
|
55
|
+
val state: SharedFlow<WebSocketState> = _state.asSharedFlow()
|
|
56
|
+
|
|
57
|
+
fun connect(url: String) {
|
|
58
|
+
val request = Request.Builder().url(url).build()
|
|
59
|
+
webSocket = okHttpClient.newWebSocket(request, object : WebSocketListener() {
|
|
60
|
+
|
|
61
|
+
override fun onOpen(webSocket: WebSocket, response: Response) {
|
|
62
|
+
_state.tryEmit(WebSocketState.Connected)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun onMessage(webSocket: WebSocket, text: String) {
|
|
66
|
+
_state.tryEmit(WebSocketState.Message(text))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
|
|
70
|
+
_state.tryEmit(WebSocketState.Error(t))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
|
74
|
+
_state.tryEmit(WebSocketState.Disconnected)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
_state.tryEmit(WebSocketState.Connecting)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fun send(message: String): Boolean {
|
|
81
|
+
return webSocket?.send(message) ?: false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun disconnect() {
|
|
85
|
+
webSocket?.close(1000, "Client disconnect")
|
|
86
|
+
webSocket = null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Ktor WebSocket (KMP)
|
|
94
|
+
|
|
95
|
+
```kotlin
|
|
96
|
+
// ✅ Ktor WebSocket session as Flow
|
|
97
|
+
class KtorWebSocketManager @Inject constructor(
|
|
98
|
+
private val client: HttpClient
|
|
99
|
+
) {
|
|
100
|
+
fun connect(url: String): Flow<WebSocketState> = callbackFlow {
|
|
101
|
+
try {
|
|
102
|
+
client.webSocket(url) {
|
|
103
|
+
trySend(WebSocketState.Connected)
|
|
104
|
+
for (frame in incoming) {
|
|
105
|
+
when (frame) {
|
|
106
|
+
is Frame.Text -> trySend(WebSocketState.Message(frame.readText()))
|
|
107
|
+
is Frame.Close -> trySend(WebSocketState.Disconnected)
|
|
108
|
+
else -> Unit
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (e: Exception) {
|
|
113
|
+
trySend(WebSocketState.Error(e))
|
|
114
|
+
} finally {
|
|
115
|
+
trySend(WebSocketState.Disconnected)
|
|
116
|
+
close()
|
|
117
|
+
}
|
|
118
|
+
awaitClose()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Reconnection with Backoff
|
|
126
|
+
|
|
127
|
+
```kotlin
|
|
128
|
+
// ✅ Auto-reconnect with exponential backoff
|
|
129
|
+
class ReconnectingWebSocket @Inject constructor(
|
|
130
|
+
private val manager: WebSocketManager
|
|
131
|
+
) {
|
|
132
|
+
private var reconnectJob: Job? = null
|
|
133
|
+
|
|
134
|
+
fun connect(url: String, scope: CoroutineScope) {
|
|
135
|
+
scope.launch {
|
|
136
|
+
manager.state.collect { state ->
|
|
137
|
+
when (state) {
|
|
138
|
+
is WebSocketState.Error,
|
|
139
|
+
is WebSocketState.Disconnected -> scheduleReconnect(url, scope)
|
|
140
|
+
else -> Unit
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
manager.connect(url)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
private fun scheduleReconnect(url: String, scope: CoroutineScope) {
|
|
148
|
+
reconnectJob?.cancel()
|
|
149
|
+
reconnectJob = scope.launch {
|
|
150
|
+
var attempt = 0
|
|
151
|
+
while (true) {
|
|
152
|
+
val delay = minOf(1000L * (2.0.pow(attempt)).toLong(), 30_000L)
|
|
153
|
+
delay(delay)
|
|
154
|
+
manager.connect(url)
|
|
155
|
+
attempt++
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Typed Message Handling
|
|
165
|
+
|
|
166
|
+
```kotlin
|
|
167
|
+
// ✅ Parse incoming messages to typed events
|
|
168
|
+
@Serializable
|
|
169
|
+
sealed interface SocketEvent {
|
|
170
|
+
@Serializable @SerialName("user_updated")
|
|
171
|
+
data class UserUpdated(val user: UserDto) : SocketEvent
|
|
172
|
+
|
|
173
|
+
@Serializable @SerialName("notification")
|
|
174
|
+
data class Notification(val message: String) : SocketEvent
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ✅ Map raw messages to typed events in ViewModel or repository
|
|
178
|
+
val events: Flow<SocketEvent> = webSocketManager.state
|
|
179
|
+
.filterIsInstance<WebSocketState.Message>()
|
|
180
|
+
.mapNotNull { runCatching { json.decodeFromString<SocketEvent>(it.data) }.getOrNull() }
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Lifecycle Integration
|
|
186
|
+
|
|
187
|
+
```kotlin
|
|
188
|
+
// ✅ Connect/disconnect tied to ViewModel lifecycle
|
|
189
|
+
class ChatViewModel @Inject constructor(
|
|
190
|
+
private val webSocket: WebSocketManager
|
|
191
|
+
) : ViewModel() {
|
|
192
|
+
|
|
193
|
+
init {
|
|
194
|
+
webSocket.connect(BuildConfig.WS_URL)
|
|
195
|
+
viewModelScope.launch {
|
|
196
|
+
webSocket.state.collect { handleState(it) }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
override fun onCleared() {
|
|
201
|
+
webSocket.disconnect()
|
|
202
|
+
super.onCleared()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Anti-Patterns
|
|
210
|
+
|
|
211
|
+
- Exposing raw `WebSocketListener` callbacks to the ViewModel — wrap in `Flow`
|
|
212
|
+
- Not handling reconnection — mobile networks drop frequently
|
|
213
|
+
- Sending messages without checking connection state — silent failures
|
|
214
|
+
- Keeping WebSocket open after the user leaves the feature — wastes battery and connection
|
|
215
|
+
- Using WebSocket for request/response patterns — use HTTP instead
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Related Skills
|
|
220
|
+
- `okhttp` — OkHttp client that powers the WebSocket
|
|
221
|
+
- `ktor` — Ktor WebSocket for KMP projects
|
|
222
|
+
- `flow` — modeling streams of messages
|
|
223
|
+
- `server-sent-events` — one-way alternative to WebSocket
|
|
224
|
+
- `retry-backoff` — backoff strategy for reconnection
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: crash-reporting
|
|
3
|
+
description: >
|
|
4
|
+
Crash reporting setup and best practices for Android using Firebase Crashlytics.
|
|
5
|
+
Load this skill when configuring Crashlytics, adding custom context to crash
|
|
6
|
+
reports, handling non-fatal errors, filtering noise, or integrating crash
|
|
7
|
+
reporting with the logging pipeline.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Crash Reporting
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Crash reporting captures uncaught exceptions and ANRs in production, providing stack traces, device info, and custom context. Firebase Crashlytics is the standard for Android. The goal is to have high signal-to-noise ratio — every reported issue should be actionable.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Report **non-fatal errors** explicitly — don't wait for crashes to find bugs
|
|
20
|
+
- Add **custom context** before crashes happen — user state, feature flags, last actions
|
|
21
|
+
- Set **user ID** on login/logout — essential for understanding crash impact
|
|
22
|
+
- Filter out **known non-actionable** crashes — third-party SDK crashes, OOM on old devices
|
|
23
|
+
- Test crash reporting in debug with a deliberate crash — don't assume it works
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
# libs.versions.toml
|
|
31
|
+
[plugins]
|
|
32
|
+
google-services = { id = "com.google.gms.google-services", version = "4.4.2" }
|
|
33
|
+
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.2" }
|
|
34
|
+
|
|
35
|
+
[libraries]
|
|
36
|
+
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics-ktx" }
|
|
37
|
+
firebase-bom = { module = "com.google.firebase:firebase-bom", version = "33.1.0" }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```kotlin
|
|
41
|
+
// app/build.gradle.kts
|
|
42
|
+
plugins {
|
|
43
|
+
alias(libs.plugins.google.services)
|
|
44
|
+
alias(libs.plugins.firebase.crashlytics)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
dependencies {
|
|
48
|
+
implementation(platform(libs.firebase.bom))
|
|
49
|
+
implementation(libs.firebase.crashlytics)
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Initialization
|
|
56
|
+
|
|
57
|
+
```kotlin
|
|
58
|
+
// ✅ Configure in Application.onCreate()
|
|
59
|
+
class App : Application() {
|
|
60
|
+
override fun onCreate() {
|
|
61
|
+
super.onCreate()
|
|
62
|
+
|
|
63
|
+
FirebaseCrashlytics.getInstance().apply {
|
|
64
|
+
// Disable in debug to avoid noise in the dashboard
|
|
65
|
+
setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Setting User Context
|
|
74
|
+
|
|
75
|
+
```kotlin
|
|
76
|
+
// ✅ Set user ID and properties on login
|
|
77
|
+
class AuthManager @Inject constructor(
|
|
78
|
+
private val crashlytics: FirebaseCrashlytics
|
|
79
|
+
) {
|
|
80
|
+
fun onUserLoggedIn(user: User) {
|
|
81
|
+
crashlytics.setUserId(user.id)
|
|
82
|
+
crashlytics.setCustomKey("user_plan", user.plan)
|
|
83
|
+
crashlytics.setCustomKey("user_region", user.region)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fun onUserLoggedOut() {
|
|
87
|
+
crashlytics.setUserId("")
|
|
88
|
+
crashlytics.setCustomKey("user_plan", "none")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Custom Keys for Context
|
|
96
|
+
|
|
97
|
+
```kotlin
|
|
98
|
+
// ✅ Set keys before operations that might crash
|
|
99
|
+
class SyncManager @Inject constructor(
|
|
100
|
+
private val crashlytics: FirebaseCrashlytics
|
|
101
|
+
) {
|
|
102
|
+
suspend fun sync(userId: String) {
|
|
103
|
+
// Set context before the risky operation
|
|
104
|
+
crashlytics.setCustomKey("sync_user_id", userId)
|
|
105
|
+
crashlytics.setCustomKey("sync_started_at", System.currentTimeMillis())
|
|
106
|
+
crashlytics.setCustomKey("sync_attempt", syncAttemptCount)
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
performSync(userId)
|
|
110
|
+
crashlytics.setCustomKey("sync_status", "success")
|
|
111
|
+
} catch (e: Exception) {
|
|
112
|
+
crashlytics.setCustomKey("sync_status", "failed")
|
|
113
|
+
throw e
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Non-Fatal Error Reporting
|
|
122
|
+
|
|
123
|
+
```kotlin
|
|
124
|
+
// ✅ Record non-fatal errors for bugs that don't crash but are wrong
|
|
125
|
+
class UserRepository @Inject constructor(
|
|
126
|
+
private val crashlytics: FirebaseCrashlytics
|
|
127
|
+
) {
|
|
128
|
+
suspend fun getUser(id: String): Result<User> {
|
|
129
|
+
return runCatching { api.getUser(id) }
|
|
130
|
+
.onFailure { error ->
|
|
131
|
+
when (error) {
|
|
132
|
+
is HttpException -> {
|
|
133
|
+
if (error.code() == 500) {
|
|
134
|
+
// Server error — report as non-fatal
|
|
135
|
+
crashlytics.recordException(error)
|
|
136
|
+
}
|
|
137
|
+
// 4xx — expected, don't report
|
|
138
|
+
}
|
|
139
|
+
is IOException -> {
|
|
140
|
+
// Network error — expected, don't report
|
|
141
|
+
}
|
|
142
|
+
else -> {
|
|
143
|
+
// Unexpected error — always report
|
|
144
|
+
crashlytics.recordException(error)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
.map { mapper.toDomain(it) }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Breadcrumbs via Log
|
|
156
|
+
|
|
157
|
+
```kotlin
|
|
158
|
+
// ✅ Add breadcrumbs — recent actions shown in crash report
|
|
159
|
+
class NavigationLogger @Inject constructor(
|
|
160
|
+
private val crashlytics: FirebaseCrashlytics
|
|
161
|
+
) {
|
|
162
|
+
fun onNavigate(route: String) {
|
|
163
|
+
// Crashlytics.log() messages appear as breadcrumbs in crash reports
|
|
164
|
+
crashlytics.log("Navigate to: $route")
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ✅ Log important user actions as breadcrumbs
|
|
169
|
+
crashlytics.log("User tapped delete on item: id=$itemId")
|
|
170
|
+
crashlytics.log("Sync started: attempt=$attempt")
|
|
171
|
+
crashlytics.log("Payment flow entered")
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Timber Integration
|
|
177
|
+
|
|
178
|
+
```kotlin
|
|
179
|
+
// ✅ CrashlyticsTree — route Timber errors to Crashlytics
|
|
180
|
+
class CrashlyticsTree : Timber.Tree() {
|
|
181
|
+
private val crashlytics = FirebaseCrashlytics.getInstance()
|
|
182
|
+
|
|
183
|
+
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
|
184
|
+
crashlytics.log("${priorityLabel(priority)}/$tag: $message")
|
|
185
|
+
|
|
186
|
+
if (priority == Log.ERROR) {
|
|
187
|
+
t?.let { crashlytics.recordException(it) }
|
|
188
|
+
?: crashlytics.recordException(Exception("Error logged without exception: $message"))
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private fun priorityLabel(priority: Int) = when (priority) {
|
|
193
|
+
Log.VERBOSE -> "V"
|
|
194
|
+
Log.DEBUG -> "D"
|
|
195
|
+
Log.INFO -> "I"
|
|
196
|
+
Log.WARN -> "W"
|
|
197
|
+
Log.ERROR -> "E"
|
|
198
|
+
else -> "?"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Anti-Patterns
|
|
206
|
+
|
|
207
|
+
- Enabling Crashlytics in debug builds — floods dashboard with non-production crashes
|
|
208
|
+
- Not setting user context — can't measure crash impact by user count
|
|
209
|
+
- Reporting all `IOException` as non-fatal — network errors are expected, creates noise
|
|
210
|
+
- Not using `setCustomKey` before risky operations — crash report lacks context
|
|
211
|
+
- Catching all exceptions silently without reporting — bugs hidden in production
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Related Skills
|
|
216
|
+
- `logging` — Timber setup that feeds into Crashlytics
|
|
217
|
+
- `structured-logging` — adding structured context to crash reports
|
|
218
|
+
- `metrics` — tracking error rates alongside crashes
|
|
219
|
+
- `firebase` — Firebase setup and configuration
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: logging
|
|
3
|
+
description: >
|
|
4
|
+
Logging strategy and implementation for Android apps.
|
|
5
|
+
Load this skill when setting up Timber, defining log levels,
|
|
6
|
+
controlling log output per build type, tagging logs consistently,
|
|
7
|
+
or deciding what should and should not be logged.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Logging
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Logging is the first line of observability in Android. Timber is the standard logging library — it wraps Android's `Log` class, adds automatic tag detection, and supports pluggable trees for different build types. In debug builds, logs go to Logcat. In release builds, logs go to Crashlytics or a remote logging service.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use **Timber** — never `Log.d()` directly in application code
|
|
20
|
+
- Never log **sensitive data** — tokens, passwords, PII, payment info
|
|
21
|
+
- Log at the **right level** — don't use `d` for errors or `e` for debug info
|
|
22
|
+
- Debug logs are **free in debug builds** — don't hold back useful context
|
|
23
|
+
- Release builds should send errors and warnings to a remote service, not Logcat
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Setup
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
# libs.versions.toml
|
|
31
|
+
[libraries]
|
|
32
|
+
timber = { module = "com.jakewharton.timber:timber", version = "5.0.1" }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```kotlin
|
|
36
|
+
// build.gradle.kts
|
|
37
|
+
dependencies {
|
|
38
|
+
implementation(libs.timber)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Initializing Timber
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
// ✅ Plant trees in Application.onCreate()
|
|
48
|
+
class App : Application() {
|
|
49
|
+
override fun onCreate() {
|
|
50
|
+
super.onCreate()
|
|
51
|
+
|
|
52
|
+
if (BuildConfig.DEBUG) {
|
|
53
|
+
Timber.plant(Timber.DebugTree()) // Logcat in debug
|
|
54
|
+
} else {
|
|
55
|
+
Timber.plant(CrashlyticsTree()) // Remote in release
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ✅ Release tree — sends warnings and errors to Crashlytics
|
|
61
|
+
class CrashlyticsTree : Timber.Tree() {
|
|
62
|
+
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
|
63
|
+
if (priority < Log.WARN) return // skip DEBUG and INFO in release
|
|
64
|
+
|
|
65
|
+
when (priority) {
|
|
66
|
+
Log.WARN -> FirebaseCrashlytics.getInstance().log("W/$tag: $message")
|
|
67
|
+
Log.ERROR -> {
|
|
68
|
+
FirebaseCrashlytics.getInstance().log("E/$tag: $message")
|
|
69
|
+
t?.let { FirebaseCrashlytics.getInstance().recordException(it) }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Log Levels
|
|
79
|
+
|
|
80
|
+
```kotlin
|
|
81
|
+
// ✅ VERBOSE — highly detailed, development only
|
|
82
|
+
Timber.v("Received ${users.size} users from cache")
|
|
83
|
+
|
|
84
|
+
// ✅ DEBUG — useful development info
|
|
85
|
+
Timber.d("Loading user profile for id=$userId")
|
|
86
|
+
|
|
87
|
+
// ✅ INFO — significant lifecycle events
|
|
88
|
+
Timber.i("User logged in: userId=$userId")
|
|
89
|
+
|
|
90
|
+
// ✅ WARN — unexpected but recoverable situation
|
|
91
|
+
Timber.w("Cache miss for key=$key, falling back to network")
|
|
92
|
+
|
|
93
|
+
// ✅ ERROR — something failed, needs attention
|
|
94
|
+
Timber.e(exception, "Failed to sync user data")
|
|
95
|
+
|
|
96
|
+
// ✅ WTF — should never happen, treat as fatal
|
|
97
|
+
Timber.wtf("Received null userId in authenticated state")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Tagging
|
|
103
|
+
|
|
104
|
+
```kotlin
|
|
105
|
+
// ✅ Timber auto-detects class name as tag — no manual tagging needed
|
|
106
|
+
class UserRepository {
|
|
107
|
+
fun loadUser() {
|
|
108
|
+
Timber.d("Loading user") // tag = "UserRepository" automatically
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ✅ Manual tag when needed (e.g. in utility functions)
|
|
113
|
+
Timber.tag("NetworkMonitor").d("Connection state: $state")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## What NOT to Log
|
|
119
|
+
|
|
120
|
+
```kotlin
|
|
121
|
+
// ❌ Never log sensitive data
|
|
122
|
+
Timber.d("Token: $accessToken") // security risk
|
|
123
|
+
Timber.d("Password entered: $password") // security risk
|
|
124
|
+
Timber.d("Card number: $cardNumber") // PCI violation
|
|
125
|
+
|
|
126
|
+
// ✅ Log safe identifiers only
|
|
127
|
+
Timber.d("User authenticated: userId=${userId.take(4)}***")
|
|
128
|
+
Timber.d("Token refreshed successfully")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Logging in Repositories and Use Cases
|
|
134
|
+
|
|
135
|
+
```kotlin
|
|
136
|
+
// ✅ Log at entry and error points
|
|
137
|
+
class UserRepositoryImpl : UserRepository {
|
|
138
|
+
|
|
139
|
+
override suspend fun getUser(id: String): Result<User> {
|
|
140
|
+
Timber.d("Fetching user: id=$id")
|
|
141
|
+
|
|
142
|
+
return runCatching {
|
|
143
|
+
api.getUser(id).also {
|
|
144
|
+
Timber.d("User fetched successfully: id=$id")
|
|
145
|
+
}
|
|
146
|
+
}.onFailure { error ->
|
|
147
|
+
Timber.e(error, "Failed to fetch user: id=$id")
|
|
148
|
+
}.map { mapper.toDomain(it) }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Anti-Patterns
|
|
156
|
+
|
|
157
|
+
- Using `Log.d()` directly — bypasses tree system, always prints in release
|
|
158
|
+
- Logging sensitive data (tokens, PII, passwords) — security and privacy violation
|
|
159
|
+
- Using `Timber.e()` for expected errors (empty state, 404) — use `Timber.w()` instead
|
|
160
|
+
- No logging in release builds — blind to production issues
|
|
161
|
+
- Over-logging — logging every line makes logs unreadable
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Related Skills
|
|
166
|
+
- `structured-logging` — structured log format for log aggregation
|
|
167
|
+
- `crash-reporting` — integrating logs with crash reports
|
|
168
|
+
- `observability` — broader observability strategy
|