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,218 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-leak-prevention
|
|
3
|
+
description: >
|
|
4
|
+
Memory leak detection and prevention in Android.
|
|
5
|
+
Load this skill when investigating memory leaks, auditing code for
|
|
6
|
+
common leak patterns, using LeakCanary, fixing Context leaks,
|
|
7
|
+
or ensuring proper cleanup of listeners and callbacks.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Memory Leak Prevention
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
A memory leak occurs when an object is no longer needed but cannot be garbage collected because something still holds a reference to it. On Android, the most common leaks involve holding a reference to an Activity or Fragment beyond their lifecycle. LeakCanary is the standard tool for detection.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Never store `Activity` or `Fragment` references in long-lived objects (ViewModel, singleton)
|
|
20
|
+
- Always unregister listeners, observers, and callbacks in the symmetric lifecycle callback
|
|
21
|
+
- Use `applicationContext` in singletons — not `Activity` context
|
|
22
|
+
- Clear ViewBinding references in `onDestroyView`
|
|
23
|
+
- Use `WeakReference` only as a last resort — fix the design instead
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## LeakCanary Setup
|
|
28
|
+
|
|
29
|
+
```toml
|
|
30
|
+
# libs.versions.toml
|
|
31
|
+
[libraries]
|
|
32
|
+
leakcanary = { module = "com.squareup.leakcanary:leakcanary-android", version = "2.14" }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```kotlin
|
|
36
|
+
// build.gradle.kts — debug only
|
|
37
|
+
dependencies {
|
|
38
|
+
debugImplementation(libs.leakcanary)
|
|
39
|
+
}
|
|
40
|
+
// No code needed — LeakCanary auto-installs via ContentProvider
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Common Leak Patterns and Fixes
|
|
46
|
+
|
|
47
|
+
```kotlin
|
|
48
|
+
// ❌ Storing Activity in a singleton — leak
|
|
49
|
+
object ImageLoader {
|
|
50
|
+
var context: Context? = null // holds Activity reference
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ✅ Use ApplicationContext
|
|
54
|
+
object ImageLoader {
|
|
55
|
+
lateinit var appContext: Context
|
|
56
|
+
|
|
57
|
+
fun init(context: Context) {
|
|
58
|
+
appContext = context.applicationContext // safe
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ❌ Anonymous listener holding Activity reference
|
|
63
|
+
class UserActivity : AppCompatActivity() {
|
|
64
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
65
|
+
super.onCreate(savedInstanceState)
|
|
66
|
+
someManager.setListener(object : SomeListener {
|
|
67
|
+
override fun onEvent() {
|
|
68
|
+
updateUi() // captures Activity implicitly
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
// listener never removed — Activity leaked
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ✅ Remove listener in onDestroy
|
|
76
|
+
class UserActivity : AppCompatActivity() {
|
|
77
|
+
private val listener = SomeListener { updateUi() }
|
|
78
|
+
|
|
79
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
80
|
+
super.onCreate(savedInstanceState)
|
|
81
|
+
someManager.addListener(listener)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
override fun onDestroy() {
|
|
85
|
+
someManager.removeListener(listener)
|
|
86
|
+
super.onDestroy()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## ViewBinding Leak
|
|
94
|
+
|
|
95
|
+
```kotlin
|
|
96
|
+
// ❌ Binding held past onDestroyView — Fragment leak
|
|
97
|
+
class UserFragment : Fragment() {
|
|
98
|
+
private lateinit var binding: FragmentUserBinding // never cleared
|
|
99
|
+
|
|
100
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
101
|
+
binding = FragmentUserBinding.bind(view)
|
|
102
|
+
}
|
|
103
|
+
// Fragment view destroyed but binding still holds reference to views
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ✅ Clear binding in onDestroyView
|
|
107
|
+
class UserFragment : Fragment() {
|
|
108
|
+
private var _binding: FragmentUserBinding? = null
|
|
109
|
+
private val binding get() = _binding!!
|
|
110
|
+
|
|
111
|
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
112
|
+
super.onViewCreated(view, savedInstanceState)
|
|
113
|
+
_binding = FragmentUserBinding.bind(view)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override fun onDestroyView() {
|
|
117
|
+
_binding = null // ✅ clear reference
|
|
118
|
+
super.onDestroyView()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Coroutine Scope Leak
|
|
126
|
+
|
|
127
|
+
```kotlin
|
|
128
|
+
// ❌ Custom scope not cancelled — coroutine leak
|
|
129
|
+
class MyManager {
|
|
130
|
+
private val scope = CoroutineScope(Dispatchers.IO)
|
|
131
|
+
|
|
132
|
+
fun start() {
|
|
133
|
+
scope.launch { doWork() }
|
|
134
|
+
}
|
|
135
|
+
// scope never cancelled — coroutines run forever
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ✅ Cancel scope when no longer needed
|
|
139
|
+
class MyManager {
|
|
140
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
141
|
+
|
|
142
|
+
fun start() {
|
|
143
|
+
scope.launch { doWork() }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fun stop() {
|
|
147
|
+
scope.cancel() // ✅ cancels all children
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Handler and Runnable Leak
|
|
155
|
+
|
|
156
|
+
```kotlin
|
|
157
|
+
// ❌ Handler posting to Activity that may be destroyed
|
|
158
|
+
class UserActivity : AppCompatActivity() {
|
|
159
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
160
|
+
|
|
161
|
+
override fun onStart() {
|
|
162
|
+
super.onStart()
|
|
163
|
+
handler.postDelayed({ updateUi() }, 5_000) // captures Activity
|
|
164
|
+
}
|
|
165
|
+
// runnable runs even after Activity destroyed
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ✅ Cancel pending callbacks in onStop
|
|
169
|
+
class UserActivity : AppCompatActivity() {
|
|
170
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
171
|
+
private val updateRunnable = Runnable { updateUi() }
|
|
172
|
+
|
|
173
|
+
override fun onStart() {
|
|
174
|
+
super.onStart()
|
|
175
|
+
handler.postDelayed(updateRunnable, 5_000)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override fun onStop() {
|
|
179
|
+
handler.removeCallbacks(updateRunnable) // ✅
|
|
180
|
+
super.onStop()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## ViewModel Holding View Reference
|
|
188
|
+
|
|
189
|
+
```kotlin
|
|
190
|
+
// ❌ ViewModel holding Composable lambda or View reference
|
|
191
|
+
class UserViewModel : ViewModel() {
|
|
192
|
+
var onSuccess: (() -> Unit)? = null // holds UI reference — leak
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ✅ Use events via Channel or SharedFlow — no direct reference
|
|
196
|
+
class UserViewModel : ViewModel() {
|
|
197
|
+
private val _events = Channel<UserEvent>(Channel.BUFFERED)
|
|
198
|
+
val events = _events.receiveAsFlow()
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Anti-Patterns
|
|
205
|
+
|
|
206
|
+
- Passing `Activity` context to a singleton or ViewModel
|
|
207
|
+
- Not removing `BroadcastReceiver` registered dynamically
|
|
208
|
+
- Not unregistering `SensorManager` or `LocationManager` listeners
|
|
209
|
+
- Storing references to Composable lambdas in ViewModel
|
|
210
|
+
- Using `registerForActivityResult` outside of `onCreate` or composable
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Related Skills
|
|
215
|
+
- `lifecycle` — symmetric resource acquisition and release
|
|
216
|
+
- `viewmodel` — ViewModel scope and what it should not hold
|
|
217
|
+
- `coroutine` — scope cancellation to prevent coroutine leaks
|
|
218
|
+
- `compose` — DisposableEffect for Compose-specific cleanup
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rendering-performance
|
|
3
|
+
description: >
|
|
4
|
+
UI rendering performance optimization in Android.
|
|
5
|
+
Load this skill when diagnosing dropped frames, reducing overdraw,
|
|
6
|
+
optimizing custom view drawing, improving scroll performance,
|
|
7
|
+
or profiling with GPU Overdraw and Frame Pacing tools.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Rendering Performance
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Android targets 60fps (16ms per frame) or 120fps (8ms per frame) on high-refresh devices. Dropped frames cause visible jank. The main causes are: work on the main thread during frame rendering, overdraw, expensive layout passes, and large hierarchies. The key tool is the **System Trace** and **GPU Overdraw** visualizer.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Keep the **main thread free** during frame rendering — offload work to coroutines
|
|
20
|
+
- Avoid **overdraw** — drawing the same pixel multiple times wastes GPU time
|
|
21
|
+
- Flatten view hierarchies — deep nesting causes expensive measure/layout passes
|
|
22
|
+
- Avoid `invalidate()` calls that trigger full redraws — invalidate only dirty regions
|
|
23
|
+
- Use hardware acceleration — it's on by default, don't disable it
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Frame Budget
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
60fps target → 16ms per frame
|
|
31
|
+
120fps target → 8ms per frame
|
|
32
|
+
|
|
33
|
+
Frame work breakdown:
|
|
34
|
+
Input handling ~2ms
|
|
35
|
+
Animation ~2ms
|
|
36
|
+
Measure/Layout ~4ms
|
|
37
|
+
Draw ~4ms
|
|
38
|
+
Sync & Upload ~2ms
|
|
39
|
+
GPU work ~2ms
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Detecting Jank
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
// ✅ FrameMetrics API — detect dropped frames programmatically
|
|
48
|
+
class PerformanceMonitor(private val activity: Activity) {
|
|
49
|
+
|
|
50
|
+
private val frameMetricsListener = Window.OnFrameMetricsAvailableListener { _, frameMetrics, _ ->
|
|
51
|
+
val totalDuration = frameMetrics.getMetric(FrameMetrics.TOTAL_DURATION) / 1_000_000L // ns to ms
|
|
52
|
+
if (totalDuration > 16) {
|
|
53
|
+
Timber.w("Slow frame: ${totalDuration}ms")
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fun start() {
|
|
58
|
+
activity.window.addOnFrameMetricsAvailableListener(
|
|
59
|
+
frameMetricsListener,
|
|
60
|
+
Handler(Looper.getMainLooper())
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun stop() {
|
|
65
|
+
activity.window.removeOnFrameMetricsAvailableListener(frameMetricsListener)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Reducing Overdraw
|
|
73
|
+
|
|
74
|
+
```kotlin
|
|
75
|
+
// ✅ Remove redundant backgrounds
|
|
76
|
+
// In Compose — don't set background on every layer
|
|
77
|
+
@Composable
|
|
78
|
+
fun Screen() {
|
|
79
|
+
// ❌ Both Scaffold and Column have background — overdraw
|
|
80
|
+
Scaffold {
|
|
81
|
+
Column(modifier = Modifier.background(Color.White)) { ... }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ✅ Only one background needed — Scaffold handles it
|
|
85
|
+
Scaffold {
|
|
86
|
+
Column { ... }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ✅ In XML theme — remove window background if screen has its own
|
|
91
|
+
// android:windowBackground="@null" in theme (if screen fills the window)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Compose Rendering Tips
|
|
97
|
+
|
|
98
|
+
```kotlin
|
|
99
|
+
// ✅ Use graphicsLayer for hardware-accelerated transformations
|
|
100
|
+
Box(
|
|
101
|
+
modifier = Modifier
|
|
102
|
+
.graphicsLayer {
|
|
103
|
+
scaleX = scale
|
|
104
|
+
scaleY = scale
|
|
105
|
+
alpha = opacity
|
|
106
|
+
// GPU-accelerated — doesn't trigger recomposition
|
|
107
|
+
}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// ✅ Avoid clip + shadow on the same composable — expensive
|
|
111
|
+
// Instead, use separate layers
|
|
112
|
+
Box(
|
|
113
|
+
modifier = Modifier
|
|
114
|
+
.shadow(elevation = 4.dp, shape = RoundedCornerShape(8.dp))
|
|
115
|
+
// shadow handles clipping internally
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
// ✅ Use drawWithCache for custom drawing that doesn't change every frame
|
|
119
|
+
Modifier.drawWithCache {
|
|
120
|
+
val path = Path()
|
|
121
|
+
path.addRoundRect(...) // computed once, reused across frames
|
|
122
|
+
onDrawBehind {
|
|
123
|
+
drawPath(path, color = Color.Blue)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Main Thread Protection
|
|
131
|
+
|
|
132
|
+
```kotlin
|
|
133
|
+
// ✅ Never block main thread during scroll/animation
|
|
134
|
+
@Composable
|
|
135
|
+
fun UserList(viewModel: UserListViewModel) {
|
|
136
|
+
val users by viewModel.users.collectAsStateWithLifecycle()
|
|
137
|
+
|
|
138
|
+
LazyColumn {
|
|
139
|
+
items(users, key = { it.id }) { user ->
|
|
140
|
+
// ❌ Don't do I/O or heavy computation here
|
|
141
|
+
// val data = File(user.avatarPath).readBytes() // blocks main thread
|
|
142
|
+
|
|
143
|
+
// ✅ Data already loaded — just render
|
|
144
|
+
UserItem(user = user)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Custom View — Dirty Region Invalidation
|
|
153
|
+
|
|
154
|
+
```kotlin
|
|
155
|
+
// ✅ Invalidate only the changed region — not the entire view
|
|
156
|
+
class ProgressView(context: Context) : View(context) {
|
|
157
|
+
private var progress = 0f
|
|
158
|
+
private val dirtyRect = Rect()
|
|
159
|
+
|
|
160
|
+
fun setProgress(value: Float) {
|
|
161
|
+
val oldRight = (progress * width).toInt()
|
|
162
|
+
progress = value
|
|
163
|
+
val newRight = (progress * width).toInt()
|
|
164
|
+
|
|
165
|
+
// Invalidate only the changed horizontal strip
|
|
166
|
+
dirtyRect.set(minOf(oldRight, newRight), 0, maxOf(oldRight, newRight), height)
|
|
167
|
+
invalidate(dirtyRect) // ✅ partial invalidation
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Profiling Tools
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
GPU Overdraw (Developer Options → Debug GPU overdraw):
|
|
178
|
+
Blue = 1x overdraw (acceptable)
|
|
179
|
+
Green = 2x overdraw (minor issue)
|
|
180
|
+
Pink = 3x overdraw (investigate)
|
|
181
|
+
Red = 4x+ overdraw (fix immediately)
|
|
182
|
+
|
|
183
|
+
System Trace (Android Studio Profiler → CPU → System Trace):
|
|
184
|
+
- See exact frame timing
|
|
185
|
+
- Identify which method causes the long frame
|
|
186
|
+
- Find binder calls, lock contention on main thread
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Anti-Patterns
|
|
192
|
+
|
|
193
|
+
- Performing disk or network I/O on the main thread — causes frame drops
|
|
194
|
+
- Deep nested `ConstraintLayout` inside `ConstraintLayout` — expensive measure passes
|
|
195
|
+
- Setting background on every layout level — causes overdraw
|
|
196
|
+
- Calling `invalidate()` on every frame when nothing changed
|
|
197
|
+
- Using `wrap_content` on `RecyclerView` — measures all items unnecessarily
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Related Skills
|
|
202
|
+
- `compose-optimization` — Compose-specific recomposition and stability
|
|
203
|
+
- `allocation-optimization` — reducing GC pauses during rendering
|
|
204
|
+
- `benchmark` — measuring frame rendering with Macrobenchmark
|
|
205
|
+
- `anr-prevention` — keeping the main thread free from blocking work
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: startup-optimization
|
|
3
|
+
description: >
|
|
4
|
+
App startup time optimization for Android.
|
|
5
|
+
Load this skill when reducing cold/warm start time, using App Startup
|
|
6
|
+
library, deferring heavy initialization, measuring startup performance,
|
|
7
|
+
or eliminating unnecessary work during application launch.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Startup Optimization
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
App startup time directly impacts user retention. Cold start (process not in memory) is the most expensive. The goal is to do the minimum work needed before the first frame is drawn, and defer everything else. Tools include the App Startup library, baseline profiles, and Macrobenchmark.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Do **minimal work** in `Application.onCreate()` — defer everything non-essential
|
|
20
|
+
- Use **App Startup library** to control and sequence initializer order
|
|
21
|
+
- Never run **network calls or disk I/O** synchronously during startup
|
|
22
|
+
- Use **Baseline Profiles** to pre-compile critical code paths
|
|
23
|
+
- Measure with **Macrobenchmark** — not manual timing
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## What Happens During Cold Start
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
1. Process creation
|
|
31
|
+
2. Application.onCreate() ← most startup time spent here
|
|
32
|
+
3. Activity.onCreate()
|
|
33
|
+
4. Layout inflation / Compose composition
|
|
34
|
+
5. First frame drawn ← TTFD (Time To First Display)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## App Startup Library
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
# libs.versions.toml
|
|
43
|
+
[libraries]
|
|
44
|
+
androidx-startup = { module = "androidx.startup:startup-runtime", version = "1.1.1" }
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```kotlin
|
|
48
|
+
// ✅ Replace manual SDK init in Application.onCreate with Initializers
|
|
49
|
+
class TimberInitializer : Initializer<Unit> {
|
|
50
|
+
override fun create(context: Context) {
|
|
51
|
+
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
|
|
52
|
+
}
|
|
53
|
+
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class AnalyticsInitializer : Initializer<Unit> {
|
|
57
|
+
override fun create(context: Context) {
|
|
58
|
+
FirebaseApp.initializeApp(context)
|
|
59
|
+
}
|
|
60
|
+
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
```xml
|
|
65
|
+
<!-- AndroidManifest.xml — register initializers -->
|
|
66
|
+
<provider
|
|
67
|
+
android:name="androidx.startup.InitializationProvider"
|
|
68
|
+
android:authorities="${applicationId}.androidx-startup"
|
|
69
|
+
android:exported="false">
|
|
70
|
+
<meta-data
|
|
71
|
+
android:name="com.example.TimberInitializer"
|
|
72
|
+
android:value="androidx.startup" />
|
|
73
|
+
<meta-data
|
|
74
|
+
android:name="com.example.AnalyticsInitializer"
|
|
75
|
+
android:value="androidx.startup" />
|
|
76
|
+
</provider>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Deferring Non-Critical Work
|
|
82
|
+
|
|
83
|
+
```kotlin
|
|
84
|
+
// ✅ Defer non-critical init to after first frame
|
|
85
|
+
class App : Application() {
|
|
86
|
+
override fun onCreate() {
|
|
87
|
+
super.onCreate()
|
|
88
|
+
// Only critical init here
|
|
89
|
+
initCrashReporting()
|
|
90
|
+
|
|
91
|
+
// Defer everything else
|
|
92
|
+
ProcessLifecycleOwner.get().lifecycle.addObserver(
|
|
93
|
+
object : DefaultLifecycleObserver {
|
|
94
|
+
override fun onStart(owner: LifecycleOwner) {
|
|
95
|
+
owner.lifecycle.removeObserver(this)
|
|
96
|
+
initNonCriticalSdks()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun initCrashReporting() {
|
|
103
|
+
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun initNonCriticalSdks() {
|
|
107
|
+
// Analytics, feature flags, etc.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Lazy Dependency Initialization with Hilt
|
|
115
|
+
|
|
116
|
+
```kotlin
|
|
117
|
+
// ✅ Use @Singleton with lazy injection — initialized on first use
|
|
118
|
+
@HiltViewModel
|
|
119
|
+
class HomeViewModel @Inject constructor(
|
|
120
|
+
private val analyticsRepository: AnalyticsRepository // injected lazily
|
|
121
|
+
) : ViewModel()
|
|
122
|
+
|
|
123
|
+
// ✅ Lazy provider for expensive dependencies
|
|
124
|
+
@Provides
|
|
125
|
+
@Singleton
|
|
126
|
+
fun provideExpensiveSdk(
|
|
127
|
+
@ApplicationContext context: Context
|
|
128
|
+
): Lazy<ExpensiveSdk> = lazy { ExpensiveSdk.init(context) }
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Splash Screen API
|
|
134
|
+
|
|
135
|
+
```kotlin
|
|
136
|
+
// ✅ Use SplashScreen API — replaces custom splash Activity
|
|
137
|
+
class MainActivity : ComponentActivity() {
|
|
138
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
139
|
+
val splashScreen = installSplashScreen()
|
|
140
|
+
|
|
141
|
+
// Keep splash visible while loading initial data
|
|
142
|
+
splashScreen.setKeepOnScreenCondition {
|
|
143
|
+
!viewModel.isReady.value
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
super.onCreate(savedInstanceState)
|
|
147
|
+
setContent { AppContent() }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```xml
|
|
153
|
+
<!-- res/values/themes.xml -->
|
|
154
|
+
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
|
|
155
|
+
<item name="windowSplashScreenBackground">@color/primary</item>
|
|
156
|
+
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_logo</item>
|
|
157
|
+
<item name="postSplashScreenTheme">@style/Theme.App</item>
|
|
158
|
+
</style>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Measuring Startup
|
|
164
|
+
|
|
165
|
+
```kotlin
|
|
166
|
+
// ✅ Macrobenchmark for startup measurement
|
|
167
|
+
@RunWith(AndroidJUnit4::class)
|
|
168
|
+
class StartupBenchmark {
|
|
169
|
+
@get:Rule
|
|
170
|
+
val benchmarkRule = MacrobenchmarkRule()
|
|
171
|
+
|
|
172
|
+
@Test
|
|
173
|
+
fun coldStartup() = benchmarkRule.measureRepeated(
|
|
174
|
+
packageName = "com.example.app",
|
|
175
|
+
metrics = listOf(StartupTimingMetric()),
|
|
176
|
+
iterations = 5,
|
|
177
|
+
startupMode = StartupMode.COLD
|
|
178
|
+
) {
|
|
179
|
+
pressHome()
|
|
180
|
+
startActivityAndWait()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## StrictMode for Development
|
|
188
|
+
|
|
189
|
+
```kotlin
|
|
190
|
+
// ✅ Enable StrictMode in debug to catch startup violations
|
|
191
|
+
if (BuildConfig.DEBUG) {
|
|
192
|
+
StrictMode.setThreadPolicy(
|
|
193
|
+
StrictMode.ThreadPolicy.Builder()
|
|
194
|
+
.detectDiskReads()
|
|
195
|
+
.detectDiskWrites()
|
|
196
|
+
.detectNetwork()
|
|
197
|
+
.penaltyLog()
|
|
198
|
+
.build()
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Anti-Patterns
|
|
206
|
+
|
|
207
|
+
- Running SharedPreferences reads synchronously in `Application.onCreate()`
|
|
208
|
+
- Initializing all SDKs eagerly — most can be deferred or lazy-initialized
|
|
209
|
+
- Custom splash Activity — use the SplashScreen API instead
|
|
210
|
+
- Not measuring startup before and after optimizations — can't prove improvement
|
|
211
|
+
- Doing Room database creation synchronously on the main thread at startup
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Related Skills
|
|
216
|
+
- `baseline-profile` — pre-compiling critical code paths
|
|
217
|
+
- `app-startup` — App Startup library details
|
|
218
|
+
- `macrobenchmark` — measuring startup and runtime performance
|
|
219
|
+
- `hilt` — lazy dependency injection at startup
|