@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.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/CHANGELOG.md +26 -0
- package/docs/facts.json +5 -5
- package/manifest.json +52 -31
- package/package.json +1 -1
- package/pipeline/skills/.skill-manifest.json +36 -20
- package/pipeline/skills/.skills-index.json +75 -9
- package/pipeline/skills/shared/README.md +13 -7
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
- package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
- package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
- package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
- package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
- package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
- package/pipeline/skills/skills-index.md +8 -2
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
# Gradle Kotlin DSL Patterns
|
|
2
|
+
|
|
3
|
+
Full code examples backing the guidance in `SKILL.md`. Headings mirror the
|
|
4
|
+
SKILL.md sections.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Version Catalog](#version-catalog)
|
|
9
|
+
- [Convention Plugins](#convention-plugins)
|
|
10
|
+
- [App Module Configuration](#app-module-configuration)
|
|
11
|
+
- [Library Module Configuration](#library-module-configuration)
|
|
12
|
+
- [Compose Configuration](#compose-configuration)
|
|
13
|
+
- [Build Variants and Flavors](#build-variants-and-flavors)
|
|
14
|
+
- [Signing Config](#signing-config)
|
|
15
|
+
- [R8 and ProGuard](#r8-and-proguard)
|
|
16
|
+
- [Build Performance](#build-performance)
|
|
17
|
+
- [Typed BuildConfig Injection](#typed-buildconfig-injection)
|
|
18
|
+
- [Typed Module Paths and Flavors](#typed-module-paths-and-flavors)
|
|
19
|
+
- [Convention Plugin Composition](#convention-plugin-composition)
|
|
20
|
+
- [Fast-Feedback Tasks](#fast-feedback-tasks)
|
|
21
|
+
|
|
22
|
+
## Version Catalog
|
|
23
|
+
|
|
24
|
+
Define all versions, libraries, and plugins in `gradle/libs.versions.toml`:
|
|
25
|
+
|
|
26
|
+
```toml
|
|
27
|
+
[versions]
|
|
28
|
+
agp = "8.7.3"
|
|
29
|
+
kotlin = "2.1.0"
|
|
30
|
+
ksp = "2.1.0-1.0.29"
|
|
31
|
+
compose-bom = "2024.12.01"
|
|
32
|
+
hilt = "2.53.1"
|
|
33
|
+
room = "2.6.1"
|
|
34
|
+
retrofit = "2.11.0"
|
|
35
|
+
okhttp = "4.12.0"
|
|
36
|
+
coroutines = "1.9.0"
|
|
37
|
+
lifecycle = "2.8.7"
|
|
38
|
+
navigation = "2.8.5"
|
|
39
|
+
serialization = "1.7.3"
|
|
40
|
+
coil = "2.7.0"
|
|
41
|
+
turbine = "1.2.0"
|
|
42
|
+
roborazzi = "1.32.2"
|
|
43
|
+
|
|
44
|
+
[libraries]
|
|
45
|
+
# Compose
|
|
46
|
+
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
|
47
|
+
compose-ui = { group = "androidx.compose.ui", name = "ui" }
|
|
48
|
+
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
|
49
|
+
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
|
50
|
+
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
|
51
|
+
compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
|
|
52
|
+
|
|
53
|
+
# Lifecycle
|
|
54
|
+
lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
|
|
55
|
+
lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" }
|
|
56
|
+
|
|
57
|
+
# Navigation
|
|
58
|
+
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
|
|
59
|
+
|
|
60
|
+
# Hilt
|
|
61
|
+
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
|
62
|
+
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
|
63
|
+
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version = "1.2.0" }
|
|
64
|
+
|
|
65
|
+
# Room
|
|
66
|
+
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
|
67
|
+
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
|
68
|
+
room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
|
69
|
+
room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
|
|
70
|
+
|
|
71
|
+
# Network
|
|
72
|
+
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
|
73
|
+
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
|
74
|
+
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
|
75
|
+
serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
|
|
76
|
+
retrofit-serialization = { group = "com.squareup.retrofit2", name = "converter-kotlinx-serialization", version = "2.11.0" }
|
|
77
|
+
|
|
78
|
+
# Coroutines
|
|
79
|
+
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutines" }
|
|
80
|
+
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
|
81
|
+
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutines" }
|
|
82
|
+
|
|
83
|
+
# Image loading
|
|
84
|
+
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
|
85
|
+
|
|
86
|
+
# Testing
|
|
87
|
+
junit = { group = "junit", name = "junit", version = "4.13.2" }
|
|
88
|
+
turbine = { group = "app.cash.turbine", name = "turbine", version.ref = "turbine" }
|
|
89
|
+
truth = { group = "com.google.truth", name = "truth", version = "1.4.4" }
|
|
90
|
+
compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
|
91
|
+
compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
|
92
|
+
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
|
|
93
|
+
roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" }
|
|
94
|
+
|
|
95
|
+
[bundles]
|
|
96
|
+
compose = ["compose-ui", "compose-ui-tooling-preview", "compose-material3", "compose-runtime"]
|
|
97
|
+
compose-debug = ["compose-ui-tooling", "compose-ui-test-manifest"]
|
|
98
|
+
lifecycle = ["lifecycle-runtime-compose", "lifecycle-viewmodel-compose"]
|
|
99
|
+
room = ["room-runtime", "room-ktx"]
|
|
100
|
+
network = ["retrofit", "okhttp", "okhttp-logging", "serialization-json", "retrofit-serialization"]
|
|
101
|
+
|
|
102
|
+
[plugins]
|
|
103
|
+
android-application = { id = "com.android.application", version.ref = "agp" }
|
|
104
|
+
android-library = { id = "com.android.library", version.ref = "agp" }
|
|
105
|
+
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
|
106
|
+
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
|
107
|
+
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
|
108
|
+
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
|
109
|
+
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
|
|
110
|
+
room = { id = "androidx.room", version.ref = "room" }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Convention Plugins
|
|
114
|
+
|
|
115
|
+
Create a `build-logic` module to share common configurations:
|
|
116
|
+
|
|
117
|
+
### settings.gradle.kts (root)
|
|
118
|
+
|
|
119
|
+
```kotlin
|
|
120
|
+
pluginManagement {
|
|
121
|
+
includeBuild("build-logic")
|
|
122
|
+
repositories {
|
|
123
|
+
google()
|
|
124
|
+
mavenCentral()
|
|
125
|
+
gradlePluginPortal()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### build-logic/settings.gradle.kts
|
|
131
|
+
|
|
132
|
+
```kotlin
|
|
133
|
+
dependencyResolutionManagement {
|
|
134
|
+
repositories {
|
|
135
|
+
google()
|
|
136
|
+
mavenCentral()
|
|
137
|
+
}
|
|
138
|
+
versionCatalogs {
|
|
139
|
+
create("libs") {
|
|
140
|
+
from(files("../gradle/libs.versions.toml"))
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
include(":convention")
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### build-logic/convention/build.gradle.kts
|
|
149
|
+
|
|
150
|
+
```kotlin
|
|
151
|
+
plugins {
|
|
152
|
+
`kotlin-dsl`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
dependencies {
|
|
156
|
+
compileOnly(libs.android.gradlePlugin)
|
|
157
|
+
compileOnly(libs.kotlin.gradlePlugin)
|
|
158
|
+
compileOnly(libs.compose.gradlePlugin)
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Android Library Convention Plugin
|
|
163
|
+
|
|
164
|
+
```kotlin
|
|
165
|
+
// build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
|
|
166
|
+
import com.android.build.gradle.LibraryExtension
|
|
167
|
+
import org.gradle.api.Plugin
|
|
168
|
+
import org.gradle.api.Project
|
|
169
|
+
import org.gradle.kotlin.dsl.configure
|
|
170
|
+
|
|
171
|
+
class AndroidLibraryConventionPlugin : Plugin<Project> {
|
|
172
|
+
override fun apply(target: Project) {
|
|
173
|
+
with(target) {
|
|
174
|
+
with(pluginManager) {
|
|
175
|
+
apply("com.android.library")
|
|
176
|
+
apply("org.jetbrains.kotlin.android")
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
extensions.configure<LibraryExtension> {
|
|
180
|
+
compileSdk = 35
|
|
181
|
+
defaultConfig {
|
|
182
|
+
minSdk = 26
|
|
183
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
184
|
+
}
|
|
185
|
+
compileOptions {
|
|
186
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
187
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Register in build-logic/convention/build.gradle.kts
|
|
196
|
+
|
|
197
|
+
```kotlin
|
|
198
|
+
gradlePlugin {
|
|
199
|
+
plugins {
|
|
200
|
+
register("androidLibrary") {
|
|
201
|
+
id = "app.android.library"
|
|
202
|
+
implementationClass = "AndroidLibraryConventionPlugin"
|
|
203
|
+
}
|
|
204
|
+
register("androidCompose") {
|
|
205
|
+
id = "app.android.compose"
|
|
206
|
+
implementationClass = "AndroidComposeConventionPlugin"
|
|
207
|
+
}
|
|
208
|
+
register("androidHilt") {
|
|
209
|
+
id = "app.android.hilt"
|
|
210
|
+
implementationClass = "AndroidHiltConventionPlugin"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Usage in Feature Module
|
|
217
|
+
|
|
218
|
+
```kotlin
|
|
219
|
+
// feature/home/build.gradle.kts
|
|
220
|
+
plugins {
|
|
221
|
+
id("app.android.library")
|
|
222
|
+
id("app.android.compose")
|
|
223
|
+
id("app.android.hilt")
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
dependencies {
|
|
227
|
+
implementation(project(":core:model"))
|
|
228
|
+
implementation(project(":core:data"))
|
|
229
|
+
implementation(project(":core:ui"))
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## App Module Configuration
|
|
234
|
+
|
|
235
|
+
```kotlin
|
|
236
|
+
// app/build.gradle.kts
|
|
237
|
+
plugins {
|
|
238
|
+
alias(libs.plugins.android.application)
|
|
239
|
+
alias(libs.plugins.kotlin.android)
|
|
240
|
+
alias(libs.plugins.compose.compiler)
|
|
241
|
+
alias(libs.plugins.hilt)
|
|
242
|
+
alias(libs.plugins.ksp)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
android {
|
|
246
|
+
namespace = "com.example.app"
|
|
247
|
+
compileSdk = 35
|
|
248
|
+
|
|
249
|
+
defaultConfig {
|
|
250
|
+
applicationId = "com.example.app"
|
|
251
|
+
minSdk = 26
|
|
252
|
+
targetSdk = 35
|
|
253
|
+
versionCode = 1
|
|
254
|
+
versionName = "1.0.0"
|
|
255
|
+
testInstrumentationRunner = "com.example.app.HiltTestRunner"
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
buildTypes {
|
|
259
|
+
release {
|
|
260
|
+
isMinifyEnabled = true
|
|
261
|
+
isShrinkResources = true
|
|
262
|
+
proguardFiles(
|
|
263
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
264
|
+
"proguard-rules.pro",
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
debug {
|
|
268
|
+
isMinifyEnabled = false
|
|
269
|
+
applicationIdSuffix = ".debug"
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
compileOptions {
|
|
274
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
275
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
kotlinOptions {
|
|
279
|
+
jvmTarget = "17"
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
dependencies {
|
|
284
|
+
implementation(platform(libs.compose.bom))
|
|
285
|
+
implementation(libs.bundles.compose)
|
|
286
|
+
implementation(libs.bundles.lifecycle)
|
|
287
|
+
implementation(libs.navigation.compose)
|
|
288
|
+
implementation(libs.hilt.android)
|
|
289
|
+
ksp(libs.hilt.compiler)
|
|
290
|
+
implementation(libs.hilt.navigation.compose)
|
|
291
|
+
|
|
292
|
+
debugImplementation(libs.bundles.compose.debug)
|
|
293
|
+
|
|
294
|
+
testImplementation(libs.junit)
|
|
295
|
+
testImplementation(libs.truth)
|
|
296
|
+
testImplementation(libs.coroutines.test)
|
|
297
|
+
testImplementation(libs.turbine)
|
|
298
|
+
|
|
299
|
+
androidTestImplementation(platform(libs.compose.bom))
|
|
300
|
+
androidTestImplementation(libs.compose.ui.test)
|
|
301
|
+
}
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Library Module Configuration
|
|
305
|
+
|
|
306
|
+
```kotlin
|
|
307
|
+
// core/data/build.gradle.kts
|
|
308
|
+
plugins {
|
|
309
|
+
alias(libs.plugins.android.library)
|
|
310
|
+
alias(libs.plugins.kotlin.android)
|
|
311
|
+
alias(libs.plugins.hilt)
|
|
312
|
+
alias(libs.plugins.ksp)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
android {
|
|
316
|
+
namespace = "com.example.core.data"
|
|
317
|
+
compileSdk = 35
|
|
318
|
+
|
|
319
|
+
defaultConfig {
|
|
320
|
+
minSdk = 26
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
dependencies {
|
|
325
|
+
implementation(project(":core:model"))
|
|
326
|
+
implementation(project(":core:network"))
|
|
327
|
+
implementation(project(":core:database"))
|
|
328
|
+
implementation(libs.hilt.android)
|
|
329
|
+
ksp(libs.hilt.compiler)
|
|
330
|
+
implementation(libs.coroutines.core)
|
|
331
|
+
|
|
332
|
+
testImplementation(libs.junit)
|
|
333
|
+
testImplementation(libs.truth)
|
|
334
|
+
testImplementation(libs.coroutines.test)
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Compose Configuration
|
|
339
|
+
|
|
340
|
+
```kotlin
|
|
341
|
+
// Convention plugin or inline
|
|
342
|
+
android {
|
|
343
|
+
buildFeatures {
|
|
344
|
+
compose = true
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// With Kotlin 2.0+, use the Compose Compiler plugin:
|
|
349
|
+
plugins {
|
|
350
|
+
alias(libs.plugins.compose.compiler)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Optional: Compose compiler metrics for performance debugging
|
|
354
|
+
// In gradle.properties or via command line:
|
|
355
|
+
// composeCompiler.reportsDestination=build/compose-reports
|
|
356
|
+
// composeCompiler.metricsDestination=build/compose-metrics
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## Build Variants and Flavors
|
|
360
|
+
|
|
361
|
+
```kotlin
|
|
362
|
+
android {
|
|
363
|
+
buildTypes {
|
|
364
|
+
release {
|
|
365
|
+
isMinifyEnabled = true
|
|
366
|
+
isShrinkResources = true
|
|
367
|
+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
|
368
|
+
}
|
|
369
|
+
debug {
|
|
370
|
+
isMinifyEnabled = false
|
|
371
|
+
applicationIdSuffix = ".debug"
|
|
372
|
+
isDebuggable = true
|
|
373
|
+
}
|
|
374
|
+
create("staging") {
|
|
375
|
+
initWith(getByName("release"))
|
|
376
|
+
applicationIdSuffix = ".staging"
|
|
377
|
+
isMinifyEnabled = true
|
|
378
|
+
matchingFallbacks += "release"
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
flavorDimensions += "environment"
|
|
383
|
+
productFlavors {
|
|
384
|
+
create("dev") {
|
|
385
|
+
dimension = "environment"
|
|
386
|
+
applicationIdSuffix = ".dev"
|
|
387
|
+
buildConfigField("String", "BASE_URL", "\"https://dev-api.example.com/\"")
|
|
388
|
+
}
|
|
389
|
+
create("prod") {
|
|
390
|
+
dimension = "environment"
|
|
391
|
+
buildConfigField("String", "BASE_URL", "\"https://api.example.com/\"")
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Signing Config
|
|
398
|
+
|
|
399
|
+
```kotlin
|
|
400
|
+
android {
|
|
401
|
+
signingConfigs {
|
|
402
|
+
create("release") {
|
|
403
|
+
storeFile = file(System.getenv("KEYSTORE_PATH") ?: "release.keystore")
|
|
404
|
+
storePassword = System.getenv("KEYSTORE_PASSWORD") ?: ""
|
|
405
|
+
keyAlias = System.getenv("KEY_ALIAS") ?: ""
|
|
406
|
+
keyPassword = System.getenv("KEY_PASSWORD") ?: ""
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
buildTypes {
|
|
411
|
+
release {
|
|
412
|
+
signingConfig = signingConfigs.getByName("release")
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Never commit keystore passwords. Use environment variables or `local.properties` (gitignored).
|
|
419
|
+
|
|
420
|
+
## R8 and ProGuard
|
|
421
|
+
|
|
422
|
+
### proguard-rules.pro
|
|
423
|
+
|
|
424
|
+
```proguard
|
|
425
|
+
# Kotlin serialization
|
|
426
|
+
-keepattributes *Annotation*, InnerClasses
|
|
427
|
+
-dontnote kotlinx.serialization.AnnotationsKt
|
|
428
|
+
|
|
429
|
+
-keepclassmembers class kotlinx.serialization.json.** {
|
|
430
|
+
*** Companion;
|
|
431
|
+
}
|
|
432
|
+
-keepclasseswithmembers class kotlinx.serialization.json.** {
|
|
433
|
+
kotlinx.serialization.KSerializer serializer(...);
|
|
434
|
+
}
|
|
435
|
+
-keep,includedescriptorclasses class com.example.app.data.dto.**$$serializer { *; }
|
|
436
|
+
-keepclassmembers class com.example.app.data.dto.** {
|
|
437
|
+
*** Companion;
|
|
438
|
+
}
|
|
439
|
+
-keepclasseswithmembers class com.example.app.data.dto.** {
|
|
440
|
+
kotlinx.serialization.KSerializer serializer(...);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# Retrofit
|
|
444
|
+
-keepattributes Signature
|
|
445
|
+
-keepattributes Exceptions
|
|
446
|
+
-keepclassmembers,allowshrinking,allowobfuscation interface * {
|
|
447
|
+
@retrofit2.http.* <methods>;
|
|
448
|
+
}
|
|
449
|
+
-dontwarn javax.annotation.**
|
|
450
|
+
-dontwarn kotlin.Unit
|
|
451
|
+
-dontwarn retrofit2.KotlinExtensions
|
|
452
|
+
-dontwarn retrofit2.KotlinExtensions$*
|
|
453
|
+
|
|
454
|
+
# OkHttp
|
|
455
|
+
-dontwarn okhttp3.internal.platform.**
|
|
456
|
+
-dontwarn org.conscrypt.**
|
|
457
|
+
-dontwarn org.bouncycastle.**
|
|
458
|
+
-dontwarn org.openjsse.**
|
|
459
|
+
|
|
460
|
+
# Hilt
|
|
461
|
+
-keep class dagger.hilt.** { *; }
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Build Performance
|
|
465
|
+
|
|
466
|
+
### gradle.properties
|
|
467
|
+
|
|
468
|
+
```properties
|
|
469
|
+
# Parallel execution
|
|
470
|
+
org.gradle.parallel=true
|
|
471
|
+
|
|
472
|
+
# Configuration cache
|
|
473
|
+
org.gradle.configuration-cache=true
|
|
474
|
+
|
|
475
|
+
# Build cache
|
|
476
|
+
org.gradle.caching=true
|
|
477
|
+
|
|
478
|
+
# JVM memory
|
|
479
|
+
org.gradle.jvmargs=-Xmx4g -XX:+UseG1GC -XX:MaxMetaspaceSize=1g
|
|
480
|
+
|
|
481
|
+
# Kotlin daemon
|
|
482
|
+
kotlin.daemon.jvmargs=-Xmx2g
|
|
483
|
+
|
|
484
|
+
# Non-transitive R classes (reduces rebuild scope)
|
|
485
|
+
android.nonTransitiveRClass=true
|
|
486
|
+
|
|
487
|
+
# Disable unused features
|
|
488
|
+
android.defaults.buildfeatures.buildconfig=false
|
|
489
|
+
android.defaults.buildfeatures.aidl=false
|
|
490
|
+
android.defaults.buildfeatures.renderscript=false
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Dependency Locking
|
|
494
|
+
|
|
495
|
+
```kotlin
|
|
496
|
+
dependencyLocking {
|
|
497
|
+
lockAllConfigurations()
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Generate lock files: ./gradlew dependencies --write-locks
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Typed BuildConfig Injection
|
|
504
|
+
|
|
505
|
+
Replace scattered `buildConfigField("String", "BASE_URL", "\"...\"")` calls with a
|
|
506
|
+
typed key enum, per-module contributor objects, and a central resolver that
|
|
507
|
+
filters by module scope and de-dupes.
|
|
508
|
+
|
|
509
|
+
```kotlin
|
|
510
|
+
// build-logic/convention/src/main/kotlin/buildconfig/BuildConfigKey.kt
|
|
511
|
+
enum class BuildConfigKey(val javaType: String) {
|
|
512
|
+
BASE_URL("String"),
|
|
513
|
+
ANALYTICS_ENABLED("boolean"),
|
|
514
|
+
API_TIMEOUT_SECONDS("int"),
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
enum class ModuleScope { APP, FEATURE, ALL }
|
|
518
|
+
|
|
519
|
+
data class BuildConfigEntry(
|
|
520
|
+
val key: BuildConfigKey,
|
|
521
|
+
val value: String,
|
|
522
|
+
val scope: ModuleScope = ModuleScope.ALL,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
interface BuildConfigContributor {
|
|
526
|
+
fun entries(flavor: Flavor): List<BuildConfigEntry>
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
object BuildConfigResolver {
|
|
530
|
+
fun resolve(
|
|
531
|
+
contributors: List<BuildConfigContributor>,
|
|
532
|
+
scope: ModuleScope,
|
|
533
|
+
flavor: Flavor,
|
|
534
|
+
): List<BuildConfigEntry> =
|
|
535
|
+
contributors
|
|
536
|
+
.flatMap { it.entries(flavor) }
|
|
537
|
+
.filter { it.scope == scope || it.scope == ModuleScope.ALL }
|
|
538
|
+
.distinctBy { it.key }
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
A contributor lives next to the module it belongs to:
|
|
543
|
+
|
|
544
|
+
```kotlin
|
|
545
|
+
object NetworkBuildConfig : BuildConfigContributor {
|
|
546
|
+
override fun entries(flavor: Flavor) = listOf(
|
|
547
|
+
BuildConfigEntry(
|
|
548
|
+
key = BuildConfigKey.BASE_URL,
|
|
549
|
+
value = "\"${flavor.baseUrl}\"",
|
|
550
|
+
),
|
|
551
|
+
BuildConfigEntry(
|
|
552
|
+
key = BuildConfigKey.API_TIMEOUT_SECONDS,
|
|
553
|
+
value = "30",
|
|
554
|
+
scope = ModuleScope.ALL,
|
|
555
|
+
),
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
The convention plugin materializes the resolved entries once per flavor, so the
|
|
561
|
+
`javaType` is always correct and no module hand-writes a `buildConfigField`:
|
|
562
|
+
|
|
563
|
+
```kotlin
|
|
564
|
+
internal fun VariantDimension.applyBuildConfig(
|
|
565
|
+
resolver: BuildConfigResolver,
|
|
566
|
+
contributors: List<BuildConfigContributor>,
|
|
567
|
+
scope: ModuleScope,
|
|
568
|
+
flavor: Flavor,
|
|
569
|
+
) {
|
|
570
|
+
resolver.resolve(contributors, scope, flavor).forEach { entry ->
|
|
571
|
+
buildConfigField(entry.key.javaType, entry.key.name, entry.value)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## Typed Module Paths and Flavors
|
|
577
|
+
|
|
578
|
+
Model module paths and flavors as sealed/enum types so builds reference
|
|
579
|
+
`Module.Core.Domain.toProject()` instead of the stringly-typed
|
|
580
|
+
`project(":core:domain")`.
|
|
581
|
+
|
|
582
|
+
```kotlin
|
|
583
|
+
// build-logic/convention/src/main/kotlin/Module.kt
|
|
584
|
+
sealed interface Module {
|
|
585
|
+
val path: String
|
|
586
|
+
|
|
587
|
+
sealed interface Core : Module {
|
|
588
|
+
data object Domain : Core { override val path = ":core:domain" }
|
|
589
|
+
data object Data : Core { override val path = ":core:data" }
|
|
590
|
+
data object Ui : Core { override val path = ":core:ui" }
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
sealed interface Feature : Module {
|
|
594
|
+
data object Home : Feature { override val path = ":feature:home" }
|
|
595
|
+
data object Profile : Feature { override val path = ":feature:profile" }
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
fun DependencyHandler.toProject(module: Module): ProjectDependency =
|
|
600
|
+
project(mapOf("path" to module.path)) as ProjectDependency
|
|
601
|
+
|
|
602
|
+
context(DependencyHandler)
|
|
603
|
+
fun Module.toProject(): ProjectDependency = toProject(this)
|
|
604
|
+
|
|
605
|
+
enum class Flavor(val flavorName: String, val dimension: String, val baseUrl: String) {
|
|
606
|
+
DEV("dev", "environment", "https://dev-api.example.com/"),
|
|
607
|
+
PROD("prod", "environment", "https://api.example.com/"),
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
```kotlin
|
|
612
|
+
// feature/home/build.gradle.kts
|
|
613
|
+
dependencies {
|
|
614
|
+
implementation(Module.Core.Domain.toProject())
|
|
615
|
+
implementation(Module.Core.Ui.toProject())
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
## Convention Plugin Composition
|
|
620
|
+
|
|
621
|
+
Higher-level plugins apply lower ones. A single `feature-impl` plugin applies
|
|
622
|
+
library + hilt + compose + test and wires the standard dependency set, so a
|
|
623
|
+
feature module's `plugins {}` block is one line.
|
|
624
|
+
|
|
625
|
+
```kotlin
|
|
626
|
+
// build-logic/convention/src/main/kotlin/FeatureImplConventionPlugin.kt
|
|
627
|
+
class FeatureImplConventionPlugin : Plugin<Project> {
|
|
628
|
+
override fun apply(target: Project) = with(target) {
|
|
629
|
+
with(pluginManager) {
|
|
630
|
+
apply("app.android.library")
|
|
631
|
+
apply("app.android.hilt")
|
|
632
|
+
apply("app.android.compose")
|
|
633
|
+
apply("app.android.test")
|
|
634
|
+
}
|
|
635
|
+
dependencies {
|
|
636
|
+
add("implementation", Module.Core.Ui.toProject(this))
|
|
637
|
+
add("implementation", Module.Core.Domain.toProject(this))
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Shared toolchain and compiler flags live in one extension both the library and
|
|
644
|
+
app plugins call:
|
|
645
|
+
|
|
646
|
+
```kotlin
|
|
647
|
+
// build-logic/convention/src/main/kotlin/KotlinAndroid.kt
|
|
648
|
+
internal fun Project.configureKotlinAndroid(
|
|
649
|
+
extension: CommonExtension<*, *, *, *, *, *>,
|
|
650
|
+
) {
|
|
651
|
+
extension.apply {
|
|
652
|
+
compileSdk = 35
|
|
653
|
+
defaultConfig { minSdk = 26 }
|
|
654
|
+
compileOptions {
|
|
655
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
656
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
extensions.configure<KotlinAndroidProjectExtension> {
|
|
660
|
+
compilerOptions {
|
|
661
|
+
jvmTarget.set(JvmTarget.JVM_17)
|
|
662
|
+
freeCompilerArgs.addAll("-Xexplicit-api=strict")
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Alias the convention plugins in the version catalog so modules apply
|
|
669
|
+
`libs.plugins.*` instead of a raw id string:
|
|
670
|
+
|
|
671
|
+
```toml
|
|
672
|
+
[plugins]
|
|
673
|
+
app-android-feature = { id = "app.android.feature", version = "unspecified" }
|
|
674
|
+
app-android-library = { id = "app.android.library", version = "unspecified" }
|
|
675
|
+
app-android-compose = { id = "app.android.compose", version = "unspecified" }
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
```kotlin
|
|
679
|
+
// feature/home/build.gradle.kts
|
|
680
|
+
plugins {
|
|
681
|
+
alias(libs.plugins.app.android.feature)
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
## Fast-Feedback Tasks
|
|
686
|
+
|
|
687
|
+
Install git hooks through a Gradle task wired as a `preBuild` dependency, so a
|
|
688
|
+
fresh clone gets the hooks on its first build. Resolve the git directory
|
|
689
|
+
correctly when `.git` is a worktree pointer file rather than a directory.
|
|
690
|
+
|
|
691
|
+
```kotlin
|
|
692
|
+
// build-logic/convention or root build.gradle.kts
|
|
693
|
+
fun resolveGitDir(root: File): File {
|
|
694
|
+
val dotGit = root.resolve(".git")
|
|
695
|
+
if (dotGit.isDirectory) return dotGit
|
|
696
|
+
val pointer = dotGit.readText().substringAfter("gitdir:").trim()
|
|
697
|
+
return File(pointer).let { if (it.isAbsolute) it else root.resolve(pointer).canonicalFile }
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
val installGitHooks by tasks.registering(Copy::class) {
|
|
701
|
+
from(rootDir.resolve("scripts/git-hooks"))
|
|
702
|
+
into(resolveGitDir(rootDir).resolve("hooks"))
|
|
703
|
+
filePermissions { unix("rwxr-xr-x") }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
subprojects {
|
|
707
|
+
tasks.matching { it.name == "preBuild" }.configureEach {
|
|
708
|
+
dependsOn(rootProject.tasks.named("installGitHooks"))
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
The build cache (`org.gradle.caching=true`) belongs in the same perf-flag set as
|
|
714
|
+
parallel execution and the configuration cache; see
|
|
715
|
+
[Build Performance](#build-performance).
|