@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/docs/facts.json +6 -6
  3. package/manifest.json +55 -34
  4. package/package.json +1 -1
  5. package/pipeline/multi-agent-refs/features/usage-reporting.md +7 -3
  6. package/pipeline/schemas/prefs.schema.json +4 -0
  7. package/pipeline/scripts/usage-register.mjs +2 -0
  8. package/pipeline/skills/.skill-manifest.json +36 -20
  9. package/pipeline/skills/.skills-index.json +75 -9
  10. package/pipeline/skills/shared/README.md +13 -7
  11. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  12. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  13. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  14. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  15. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  16. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  17. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  18. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  19. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  20. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  21. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  22. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  23. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  24. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  25. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  26. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  27. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  28. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  29. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  30. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  31. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  32. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  33. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  34. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  35. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  36. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  37. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  38. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  39. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  40. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  41. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  42. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  43. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  44. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  45. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  46. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  47. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  48. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  49. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  50. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  51. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  52. package/pipeline/skills/skills-index.md +8 -2
@@ -9,6 +9,10 @@ Performance optimization patterns for Android apps targeting Jetpack Compose,
9
9
  R8, Baseline Profiles, and modern profiling tools. Covers startup, runtime,
10
10
  memory, and image loading optimization.
11
11
 
12
+ Detailed code examples live in `references/patterns.md`, organized under the
13
+ same section headings used below. Load the matching section when you implement
14
+ or review that area; the guidance here is enough to decide what to do.
15
+
12
16
  ## Contents
13
17
 
14
18
  - [Baseline Profiles](#baseline-profiles)
@@ -27,88 +31,15 @@ memory, and image loading optimization.
27
31
  ## Baseline Profiles
28
32
 
29
33
  Baseline Profiles pre-compile critical code paths at install time, improving
30
- startup time and reducing jank on first run.
31
-
32
- ### Setup
33
-
34
- ```kotlin
35
- // build.gradle.kts (:app)
36
- plugins {
37
- alias(libs.plugins.android.application)
38
- alias(libs.plugins.baselineprofile)
39
- }
40
-
41
- dependencies {
42
- baselineProfile(project(":baselineprofile"))
43
- }
44
-
45
- baselineProfile {
46
- automaticGenerationDuringBuild = true
47
- }
48
- ```
49
-
50
- ### Generator Module
51
-
52
- ```kotlin
53
- // :baselineprofile/build.gradle.kts
54
- plugins {
55
- alias(libs.plugins.android.test)
56
- alias(libs.plugins.baselineprofile)
57
- }
58
-
59
- android {
60
- namespace = "com.example.baselineprofile"
61
- targetProjectPath = ":app"
62
- }
63
-
64
- baselineProfile {
65
- useConnectedDevices = true
66
- }
67
- ```
68
-
69
- ### Profile Generator
70
-
71
- ```kotlin
72
- @RunWith(AndroidJUnit4::class)
73
- @LargeTest
74
- class BaselineProfileGenerator {
75
-
76
- @get:Rule
77
- val rule = BaselineProfileRule()
78
-
79
- @Test
80
- fun generateBaselineProfile() {
81
- rule.collect(
82
- packageName = "com.example.app",
83
- includeInStartupProfile = true,
84
- ) {
85
- // Cold start
86
- pressHome()
87
- startActivityAndWait()
88
-
89
- // Critical user journeys
90
- device.findObject(By.text("Search")).click()
91
- device.waitForIdle()
92
-
93
- device.findObject(By.res("search_field")).text = "Istanbul"
94
- device.waitForIdle()
95
-
96
- device.findObject(By.res("flight_card")).click()
97
- device.waitForIdle()
98
- }
99
- }
100
- }
101
- ```
102
-
103
- ### Generate and Apply
34
+ startup time and reducing jank on first run. Apply the `baselineprofile` plugin
35
+ to `:app`, add a `:baselineprofile` test module that targets `:app`, and write a
36
+ `BaselineProfileRule` generator that exercises cold start plus the critical user
37
+ journeys. Run `./gradlew :app:generateBaselineProfile` to produce
38
+ `baseline-prof.txt` and `startup-prof.txt` under `app/src/main/`.
104
39
 
105
- ```bash
106
- # Generate baseline profile
107
- ./gradlew :app:generateBaselineProfile
108
-
109
- # Profile is written to app/src/main/baseline-prof.txt
110
- # Startup profile to app/src/main/startup-prof.txt
111
- ```
40
+ See `references/patterns.md` -> "Baseline Profiles" for the `:app` and
41
+ generator `build.gradle.kts`, the `BaselineProfileGenerator` test, and the
42
+ generate command.
112
43
 
113
44
  ## Compose Stability
114
45
 
@@ -125,564 +56,117 @@ For this to work, parameters must be **stable** (immutable or observable).
125
56
  | `class` (non-data) | No | Compose cannot infer stability |
126
57
  | Lambda `() -> Unit` | Unstable by default | Captured variables may change |
127
58
 
128
- ### @Immutable and @Stable Annotations
129
-
130
- ```kotlin
131
- // Mark a class as truly immutable (all properties never change after construction)
132
- @Immutable
133
- data class FlightUiModel(
134
- val id: String,
135
- val route: String,
136
- val price: String,
137
- val status: String,
138
- )
139
-
140
- // Mark a class as stable (Compose can track changes via equals)
141
- @Stable
142
- data class FilterState(
143
- val origin: String = "",
144
- val destination: String = "",
145
- val date: LocalDate? = null,
146
- )
147
- ```
148
-
149
- ### Use kotlinx.collections.immutable
59
+ Mark truly immutable models `@Immutable` and observable-but-stable state
60
+ `@Stable`. Replace `List`/`Set`/`Map` parameters with
61
+ `kotlinx.collections.immutable` variants (`ImmutableList`, `persistentListOf()`)
62
+ so list-carrying UI state stays stable. On Kotlin 2.0+ the Compose Compiler
63
+ enables strong skipping by default (unstable params compared by `===`, lambdas
64
+ memoized), reducing the need for manual annotations. Verify results in the
65
+ Compose compiler reports.
150
66
 
151
- Replace `List`, `Set`, `Map` with immutable variants for stable parameters:
152
-
153
- ```kotlin
154
- // build.gradle.kts
155
- implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.8")
156
-
157
- // Usage
158
- @Immutable
159
- data class FlightListUiState(
160
- val flights: ImmutableList<FlightUiModel> = persistentListOf(),
161
- val isLoading: Boolean = false,
162
- )
163
-
164
- // Convert in ViewModel
165
- val uiState: StateFlow<FlightListUiState> = repository.getFlights()
166
- .map { flights ->
167
- FlightListUiState(
168
- flights = flights.map(::toUiModel).toImmutableList(),
169
- )
170
- }
171
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), FlightListUiState())
172
- ```
173
-
174
- ### Strong Skipping Mode (Kotlin 2.0+)
175
-
176
- With the Compose Compiler plugin for Kotlin 2.0+, strong skipping is enabled
177
- by default. This makes more composables skippable:
178
-
179
- - Unstable parameters are compared by instance equality (`===`)
180
- - Lambdas are memoized by default
181
- - Less need for manual `@Stable` / `@Immutable` annotations
182
-
183
- Verify in Compose compiler reports:
184
-
185
- ```properties
186
- # gradle.properties
187
- composeCompiler.reportsDestination=build/compose-reports
188
- composeCompiler.metricsDestination=build/compose-metrics
189
- ```
190
-
191
- ```bash
192
- ./gradlew assembleRelease
193
- # Check build/compose-reports/app_release-composables.txt for skippability
194
- ```
67
+ See `references/patterns.md` -> "Compose Stability" for the annotation
68
+ examples, the immutable-collections `StateFlow` mapping, and the
69
+ `gradle.properties` report settings.
195
70
 
196
71
  ## Recomposition Debugging
197
72
 
198
- ### Layout Inspector (Android Studio)
199
-
200
- 1. Run app in debug mode
201
- 2. Open Layout Inspector: View > Tool Windows > Layout Inspector
202
- 3. Enable "Show Recomposition Counts" in the toolbar
203
- 4. Interact with the app and observe which composables recompose
204
-
205
- High recomposition counts on composables that should not change indicate
206
- unnecessary invalidation.
207
-
208
- ### Recomposition Highlighter
209
-
210
- ```kotlin
211
- // Debug utility -- remove before release
212
- @Composable
213
- fun RecompositionCounter(label: String) {
214
- val count = remember { mutableIntStateOf(0) }
215
- count.intValue++
216
- SideEffect {
217
- Log.d("Recomposition", "$label: ${count.intValue}")
218
- }
219
- }
220
- ```
221
-
222
- ### Compose Compiler Metrics
223
-
224
- Generate stability reports to identify unstable composables:
225
-
226
- ```bash
227
- ./gradlew assembleRelease \
228
- -PcomposeCompiler.reportsDestination=build/compose-reports \
229
- -PcomposeCompiler.metricsDestination=build/compose-metrics
230
- ```
231
-
232
- Key files:
233
- - `*-composables.txt`: Lists all composables with restartable/skippable status
234
- - `*-classes.txt`: Lists all classes with stability status
235
- - `*-module.json`: Summary metrics
236
-
237
- Look for composables marked `restartable` but NOT `skippable` -- these are
73
+ Use Android Studio's Layout Inspector with "Show Recomposition Counts" enabled
74
+ to find composables that recompose more than they should. For targeted logging,
75
+ a small `RecompositionCounter` debug composable prints per-label counts (remove
76
+ before release). For a build-wide view, generate Compose compiler metrics and
77
+ look for composables marked `restartable` but NOT `skippable` -- those are the
238
78
  recomposition hotspots.
239
79
 
240
- ## Lazy Layout Performance
80
+ See `references/patterns.md` -> "Recomposition Debugging" for the Layout
81
+ Inspector steps, the `RecompositionCounter` snippet, the metrics command, and
82
+ the report files to read.
241
83
 
242
- ### LazyColumn/LazyRow Keys
84
+ ## Lazy Layout Performance
243
85
 
244
- Always provide stable keys for lazy layouts. Without keys, Compose cannot
245
- efficiently diff items.
86
+ Always provide a stable `key` for `LazyColumn`/`LazyRow` items; without it
87
+ Compose cannot diff efficiently and re-renders the whole list on any change.
246
88
 
247
89
  ```kotlin
248
- // CORRECT: stable key from item ID
249
90
  LazyColumn {
250
- items(
251
- items = flights,
252
- key = { flight -> flight.id },
253
- ) { flight ->
91
+ items(items = flights, key = { it.id }) { flight ->
254
92
  FlightCard(flight = flight)
255
93
  }
256
94
  }
257
-
258
- // WRONG: no key -- full list diff on every change
259
- LazyColumn {
260
- items(flights) { flight ->
261
- FlightCard(flight = flight)
262
- }
263
- }
264
- ```
265
-
266
- ### contentType for Heterogeneous Lists
267
-
268
- ```kotlin
269
- LazyColumn {
270
- items(
271
- items = feedItems,
272
- key = { it.id },
273
- contentType = { item ->
274
- when (item) {
275
- is FeedItem.Flight -> "flight"
276
- is FeedItem.Hotel -> "hotel"
277
- is FeedItem.Ad -> "ad"
278
- }
279
- },
280
- ) { item ->
281
- when (item) {
282
- is FeedItem.Flight -> FlightCard(item)
283
- is FeedItem.Hotel -> HotelCard(item)
284
- is FeedItem.Ad -> AdBanner(item)
285
- }
286
- }
287
- }
288
95
  ```
289
96
 
290
- `contentType` enables Compose to reuse view holders of the same type,
291
- similar to RecyclerView's view type system.
292
-
293
- ### Avoid Heavy Computation in Item Scope
294
-
295
- ```kotlin
296
- // WRONG: formatting on every recomposition
297
- items(flights, key = { it.id }) { flight ->
298
- val formatted = SimpleDateFormat("HH:mm", Locale.getDefault())
299
- .format(Date(flight.departureTime))
300
- Text(formatted)
301
- }
302
-
303
- // CORRECT: precompute in ViewModel or UiModel
304
- data class FlightUiModel(
305
- val id: String,
306
- val formattedDeparture: String, // precomputed
307
- )
308
- ```
97
+ For heterogeneous lists, add `contentType` so Compose reuses layouts of the
98
+ same type (like RecyclerView view types). Never format or allocate inside item
99
+ scope -- precompute display strings in the `UiModel`. Default prefetch is
100
+ usually sufficient.
309
101
 
310
- ### Prefetch Configuration
311
-
312
- ```kotlin
313
- LazyColumn(
314
- state = rememberLazyListState(),
315
- // Default prefetch is usually sufficient
316
- // For custom behavior:
317
- flingBehavior = ScrollableDefaults.flingBehavior(),
318
- ) {
319
- items(flights, key = { it.id }) { flight ->
320
- FlightCard(flight = flight)
321
- }
322
- }
323
- ```
102
+ See `references/patterns.md` -> "Lazy Layout Performance" for the keyed vs
103
+ unkeyed contrast, the `contentType` example, the precompute-in-UiModel pattern,
104
+ and prefetch configuration.
324
105
 
325
106
  ## R8 Optimization
326
107
 
327
108
  R8 performs code shrinking, obfuscation, and optimization in release builds.
109
+ Enable it with `isMinifyEnabled = true` and `isShrinkResources = true` on the
110
+ `release` build type, using `proguard-android-optimize.txt` plus your keep
111
+ rules. Turn on `android.enableR8.fullMode=true` for more aggressive
112
+ optimization (may require extra keep rules for reflection-based code). Measure
113
+ impact by comparing debug and release APK sizes.
328
114
 
329
- ### Enable Full Optimization
330
-
331
- ```kotlin
332
- android {
333
- buildTypes {
334
- release {
335
- isMinifyEnabled = true
336
- isShrinkResources = true
337
- proguardFiles(
338
- getDefaultProguardFile("proguard-android-optimize.txt"),
339
- "proguard-rules.pro",
340
- )
341
- }
342
- }
343
- }
344
- ```
345
-
346
- ### R8 Full Mode
347
-
348
- R8 full mode enables more aggressive optimizations:
349
-
350
- ```properties
351
- # gradle.properties
352
- android.enableR8.fullMode=true
353
- ```
354
-
355
- Full mode may require additional keep rules for reflection-based code.
356
-
357
- ### Measuring R8 Impact
358
-
359
- ```bash
360
- # Before R8 (debug APK)
361
- ./gradlew assembleDebug
362
- ls -la app/build/outputs/apk/debug/app-debug.apk
363
-
364
- # After R8 (release APK)
365
- ./gradlew assembleRelease
366
- ls -la app/build/outputs/apk/release/app-release.apk
367
-
368
- # Compare sizes
369
- ```
115
+ See `references/patterns.md` -> "R8 Optimization" for the `buildTypes` block,
116
+ the full-mode property, and the size-comparison commands.
370
117
 
371
118
  ## Memory Leak Detection
372
119
 
373
- ### LeakCanary Setup
374
-
375
- ```kotlin
376
- // build.gradle.kts
377
- debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
378
- ```
379
-
380
- LeakCanary runs automatically in debug builds. It detects:
381
- - Activity leaks
382
- - Fragment leaks
383
- - ViewModel leaks
384
- - Service leaks
385
- - Custom watched objects
386
-
387
- ### Common Leak Patterns
388
-
389
- ```kotlin
390
- // LEAK: Activity reference in singleton
391
- object Analytics {
392
- private var context: Context? = null // holds Activity reference
393
- fun init(context: Context) { this.context = context }
394
- }
395
-
396
- // FIX: Use application context
397
- object Analytics {
398
- private var context: Context? = null
399
- fun init(context: Context) { this.context = context.applicationContext }
400
- }
401
-
402
- // LEAK: Coroutine scope outlives lifecycle
403
- class MyActivity : ComponentActivity() {
404
- init {
405
- GlobalScope.launch { // outlives Activity
406
- // long-running work
407
- }
408
- }
409
- }
410
-
411
- // FIX: Use lifecycleScope
412
- class MyActivity : ComponentActivity() {
413
- override fun onCreate(savedInstanceState: Bundle?) {
414
- super.onCreate(savedInstanceState)
415
- lifecycleScope.launch {
416
- // cancelled when Activity is destroyed
417
- }
418
- }
419
- }
420
-
421
- // LEAK: Anonymous inner class holds reference
422
- class MyViewModel : ViewModel() {
423
- val callback = object : Callback {
424
- override fun onResult(data: Data) {
425
- // this holds implicit reference to MyViewModel
426
- }
427
- }
428
- }
429
- ```
430
-
431
- ### Manual Object Watching
120
+ Add LeakCanary as `debugImplementation` only; it runs automatically in debug
121
+ builds and reports Activity, Fragment, ViewModel, Service, and custom watched
122
+ leaks. The recurring leak sources are: Activity/Context references held in
123
+ singletons (fix with `applicationContext`), `GlobalScope` coroutines that
124
+ outlive a lifecycle (fix with `lifecycleScope`/`viewModelScope`), and anonymous
125
+ inner classes capturing an implicit outer reference. For custom objects, watch
126
+ them explicitly with `objectWatcher.expectWeaklyReachable(...)`.
432
127
 
433
- ```kotlin
434
- // Watch custom objects for leaks
435
- val watcher = LeakCanary.objectWatcher
436
- watcher.expectWeaklyReachable(myObject, "MyObject should be GC'd")
437
- ```
128
+ See `references/patterns.md` -> "Memory Leak Detection" for the dependency
129
+ line, the leak-vs-fix pairs, and the manual object-watching snippet.
438
130
 
439
131
  ## Startup Optimization
440
132
 
441
- ### Macrobenchmark for Startup Tracing
442
-
443
- ```kotlin
444
- @RunWith(AndroidJUnit4::class)
445
- class StartupBenchmark {
446
-
447
- @get:Rule
448
- val benchmarkRule = MacrobenchmarkRule()
449
-
450
- @Test
451
- fun startupCompilation() {
452
- benchmarkRule.measureRepeated(
453
- packageName = "com.example.app",
454
- metrics = listOf(StartupTimingMetric()),
455
- iterations = 5,
456
- startupMode = StartupMode.COLD,
457
- ) {
458
- pressHome()
459
- startActivityAndWait()
460
- }
461
- }
462
-
463
- @Test
464
- fun startupWithScrolling() {
465
- benchmarkRule.measureRepeated(
466
- packageName = "com.example.app",
467
- metrics = listOf(
468
- StartupTimingMetric(),
469
- FrameTimingMetric(),
470
- ),
471
- iterations = 5,
472
- startupMode = StartupMode.COLD,
473
- ) {
474
- pressHome()
475
- startActivityAndWait()
476
-
477
- val list = device.findObject(By.res("flight_list"))
478
- list.setGestureMargin(device.displayWidth / 5)
479
- list.fling(Direction.DOWN)
480
- device.waitForIdle()
481
- }
482
- }
483
- }
484
- ```
485
-
486
- ### Startup Best Practices
487
-
488
- ```kotlin
489
- @HiltAndroidApp
490
- class MyApplication : Application() {
491
- override fun onCreate() {
492
- super.onCreate()
493
- // Only initialize essentials here
494
- // Defer non-critical init to background
495
- }
496
- }
497
-
498
- // Use App Startup library for lazy initialization
499
- class AnalyticsInitializer : Initializer<Analytics> {
500
- override fun create(context: Context): Analytics {
501
- return Analytics.init(context)
502
- }
503
-
504
- override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
505
- }
506
- ```
507
-
508
- ### Defer Heavy Initialization
509
-
510
- ```kotlin
511
- // WRONG: blocking startup
512
- class MyApplication : Application() {
513
- override fun onCreate() {
514
- super.onCreate()
515
- Database.initialize(this) // slow
516
- ImageLoader.setup(this) // slow
517
- CrashReporting.setup(this) // slow
518
- }
519
- }
133
+ Measure cold start with a Macrobenchmark `StartupTimingMetric` (add
134
+ `FrameTimingMetric` to catch scroll jank during startup), running against a
135
+ release build on a real device. Keep `Application.onCreate` minimal: initialize
136
+ only essentials, defer database, image loader, and other heavy setup to a
137
+ background dispatcher (or the App Startup library). Crash reporting can usually
138
+ stay on main.
520
139
 
521
- // CORRECT: defer to background
522
- class MyApplication : Application() {
523
- override fun onCreate() {
524
- super.onCreate()
525
- ProcessLifecycleOwner.get().lifecycleScope.launch(Dispatchers.Default) {
526
- Database.initialize(this@MyApplication)
527
- ImageLoader.setup(this@MyApplication)
528
- }
529
- // Crash reporting can stay on main (usually lightweight)
530
- CrashReporting.setup(this)
531
- }
532
- }
533
- ```
140
+ See `references/patterns.md` -> "Startup Optimization" for the
141
+ `StartupBenchmark` tests, the App Startup `Initializer`, and the
142
+ blocking-vs-deferred `Application.onCreate` contrast.
534
143
 
535
144
  ## Image Loading with Coil
536
145
 
537
- ### Basic Setup
538
-
539
- ```kotlin
540
- // build.gradle.kts
541
- implementation("io.coil-kt:coil-compose:2.7.0")
542
-
543
- // Composable
544
- @Composable
545
- fun FlightImage(imageUrl: String, modifier: Modifier = Modifier) {
546
- AsyncImage(
547
- model = ImageRequest.Builder(LocalContext.current)
548
- .data(imageUrl)
549
- .crossfade(true)
550
- .memoryCacheKey(imageUrl)
551
- .diskCacheKey(imageUrl)
552
- .build(),
553
- contentDescription = "Flight image",
554
- modifier = modifier,
555
- contentScale = ContentScale.Crop,
556
- )
557
- }
558
- ```
559
-
560
- ### Global Image Loader Configuration
561
-
562
- ```kotlin
563
- @HiltAndroidApp
564
- class MyApplication : Application(), ImageLoaderFactory {
565
-
566
- override fun newImageLoader(): ImageLoader =
567
- ImageLoader.Builder(this)
568
- .memoryCache {
569
- MemoryCache.Builder(this)
570
- .maxSizePercent(0.25) // 25% of app memory
571
- .build()
572
- }
573
- .diskCache {
574
- DiskCache.Builder()
575
- .directory(cacheDir.resolve("image_cache"))
576
- .maxSizePercent(0.02) // 2% of disk
577
- .build()
578
- }
579
- .crossfade(true)
580
- .respectCacheHeaders(true)
581
- .build()
582
- }
583
- ```
584
-
585
- ### Image Size Optimization
586
-
587
- ```kotlin
588
- // Downscale to view size -- prevents loading full-resolution images
589
- AsyncImage(
590
- model = ImageRequest.Builder(LocalContext.current)
591
- .data(imageUrl)
592
- .size(Size(200, 200)) // Request specific size
593
- .scale(Scale.FILL)
594
- .build(),
595
- contentDescription = null,
596
- modifier = Modifier.size(100.dp), // Display size
597
- )
598
- ```
599
-
600
- ### Placeholder and Error States
146
+ Load images with Coil's `AsyncImage`, enabling `crossfade` and explicit
147
+ cache keys. Configure a global `ImageLoader` via `ImageLoaderFactory` with
148
+ memory and disk cache limits (e.g. 25% of app memory, 2% of disk). Always
149
+ request images at display size with `.size(...)` to avoid decoding
150
+ full-resolution bitmaps, and supply `placeholder`/`error`/`fallback` painters.
601
151
 
602
- ```kotlin
603
- AsyncImage(
604
- model = imageUrl,
605
- contentDescription = "Airline logo",
606
- placeholder = painterResource(R.drawable.placeholder_airline),
607
- error = painterResource(R.drawable.error_airline),
608
- fallback = painterResource(R.drawable.default_airline),
609
- modifier = Modifier
610
- .size(48.dp)
611
- .clip(CircleShape),
612
- )
613
- ```
152
+ See `references/patterns.md` -> "Image Loading with Coil" for the basic
153
+ `AsyncImage`, the `ImageLoaderFactory` configuration, the size-optimization
154
+ request, and the placeholder/error states.
614
155
 
615
156
  ## General Performance Patterns
616
157
 
617
- ### derivedStateOf for Expensive Computations
618
-
619
- ```kotlin
620
- @Composable
621
- fun FlightList(flights: List<Flight>) {
622
- val listState = rememberLazyListState()
623
-
624
- // Only recalculates when the derived condition changes
625
- val showScrollToTop by remember {
626
- derivedStateOf { listState.firstVisibleItemIndex > 5 }
627
- }
628
-
629
- Box {
630
- LazyColumn(state = listState) {
631
- items(flights, key = { it.id }) { FlightCard(it) }
632
- }
633
-
634
- if (showScrollToTop) {
635
- FloatingActionButton(
636
- onClick = { /* scroll to top */ },
637
- modifier = Modifier.align(Alignment.BottomEnd),
638
- ) {
639
- Icon(Icons.Default.ArrowUpward, contentDescription = "Scroll to top")
640
- }
641
- }
642
- }
643
- }
644
- ```
645
-
646
- ### remember for Expensive Objects
647
-
648
- ```kotlin
649
- // WRONG: recreated every recomposition
650
- @Composable
651
- fun DateDisplay(timestamp: Long) {
652
- val formatter = SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
653
- Text(formatter.format(Date(timestamp)))
654
- }
655
-
656
- // CORRECT: remembered across recompositions
657
- @Composable
658
- fun DateDisplay(timestamp: Long) {
659
- val formatter = remember { SimpleDateFormat("dd MMM yyyy", Locale.getDefault()) }
660
- Text(formatter.format(Date(timestamp)))
661
- }
662
- ```
663
-
664
- ### Debounce for Search
665
-
666
- ```kotlin
667
- @HiltViewModel
668
- class SearchViewModel @Inject constructor(
669
- private val searchUseCase: SearchFlightsUseCase,
670
- ) : ViewModel() {
671
-
672
- private val searchQuery = MutableStateFlow("")
673
-
674
- val searchResults: StateFlow<List<Flight>> = searchQuery
675
- .debounce(300) // Wait 300ms after last keystroke
676
- .distinctUntilChanged()
677
- .filter { it.length >= 2 }
678
- .flatMapLatest { query -> searchUseCase(query) }
679
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
680
-
681
- fun onQueryChange(query: String) {
682
- searchQuery.value = query
683
- }
684
- }
685
- ```
158
+ - **`derivedStateOf`** for values computed from state that change less often
159
+ than their inputs (e.g. a scroll-to-top flag derived from
160
+ `firstVisibleItemIndex`), so recomposition triggers only on the derived
161
+ change.
162
+ - **`remember`** expensive objects (formatters, regex) so they survive
163
+ recomposition instead of being reallocated each pass.
164
+ - **Debounce** search input in the ViewModel (`debounce(300)` +
165
+ `distinctUntilChanged()` + `flatMapLatest`) to avoid a request per keystroke.
166
+
167
+ See `references/patterns.md` -> "General Performance Patterns" for the
168
+ `derivedStateOf` list example, the `remember` formatter contrast, and the
169
+ debounced search `StateFlow`.
686
170
 
687
171
  ## Do's and Don'ts
688
172