# Android Design Tokens Codegen - Patterns

Copy-ready Gradle and Kotlin for the schema-to-typed-code pipeline. Each heading
mirrors a section in `SKILL.md`. All input is a local JSON schema; all output is
generated Kotlin under the build directory. Names are illustrative and generic.

## Task wrapper

A typed task whose only job is declaring inputs/outputs and invoking a generator.

```kotlin
abstract class GenerateFromSchemaTask : DefaultTask() {

    @get:InputFile
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val schema: RegularFileProperty

    @get:Classpath
    abstract val generatorClasspath: ConfigurableFileCollection

    @get:Input
    abstract val packageName: Property<String>

    @get:Input
    abstract val checkMode: Property<Boolean>

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @get:Inject
    abstract val execOps: ExecOperations

    @TaskAction
    fun run() {
        execOps.javaexec {
            classpath = generatorClasspath
            mainClass.set("com.example.codegen.MainKt")
            args(
                "--schema", schema.get().asFile.absolutePath,
                "--package", packageName.get(),
                "--out", outputDir.get().asFile.absolutePath,
            )
            if (checkMode.get()) args("--check")
        }
    }
}
```

### Registration with a resolvable generator configuration

```kotlin
val codegen: Configuration by configurations.creating { isCanBeConsumed = false }

dependencies {
    codegen("com.example:token-codegen:1.4.0")
}

fun registerGenerator(name: String, schemaPath: String, out: String, pkg: String) =
    tasks.register<GenerateFromSchemaTask>(name) {
        schema.set(layout.projectDirectory.file(schemaPath))
        generatorClasspath.from(codegen)
        packageName.set(pkg)
        outputDir.set(layout.buildDirectory.dir("generated/source/$name"))
        checkMode.set(providers.gradleProperty("codegen.check").isPresent)
    }
```

### WorkAction isolation (generator runs in a worker, keeps daemon clean)

```kotlin
interface GenerateParams : WorkParameters {
    val schema: RegularFileProperty
    val outputDir: DirectoryProperty
    val checkMode: Property<Boolean>
}

abstract class GenerateAction : WorkAction<GenerateParams> {
    override fun execute() {
        val out = parameters.outputDir.get().asFile
        // parse parameters.schema, emit into out, or compare when checkMode is set
    }
}

// inside @TaskAction:
workerExecutor.classLoaderIsolation { classpath.from(generatorClasspath) }
    .submit(GenerateAction::class.java) {
        schema.set(this@GenerateFromSchemaTask.schema)
        outputDir.set(this@GenerateFromSchemaTask.outputDir)
        checkMode.set(this@GenerateFromSchemaTask.checkMode)
    }
```

## preBuild wiring

Register every generator's output as a variant generated-source directory and
make `preBuild` depend on all of them.

```kotlin
val generators = listOf(
    registerGenerator("generateDesignTokens", "schema/tokens.json",    "gen/tokens",    "com.example.tokens"),
    registerGenerator("generateTestIds",      "schema/test-ids.json",  "gen/testids",   "com.example.testids"),
    registerGenerator("generateAnalytics",    "schema/analytics.json", "gen/analytics", "com.example.analytics"),
    registerGenerator("generateDeeplinks",    "schema/deeplinks.json", "gen/deeplinks", "com.example.deeplinks"),
)

androidComponents.onVariants { variant ->
    generators.forEach { gen ->
        variant.sources.java?.addGeneratedSourceDirectory(gen, GenerateFromSchemaTask::outputDir)
    }
}

tasks.named("preBuild").configure { dependsOn(generators) }
```

Ordering when a KSP or annotation processor consumes generated types: the
`addGeneratedSourceDirectory` call already wires KSP/kapt to depend on the
generator, so generated tokens are visible to processors without extra
`mustRunAfter`. Only add explicit ordering if a generator reads another
generator's output, which is an anti-pattern to avoid.

## Check mode

The check task regenerates into a temp dir and compares, failing on any diff.
Prefer building this into the generator binary (`--check`) so the same code path
produces and verifies. A Gradle-side comparison when the binary lacks it:

```kotlin
abstract class VerifyCodegenTask : DefaultTask() {

    @get:InputDirectory abstract val committed: DirectoryProperty
    @get:InputDirectory abstract val regenerated: DirectoryProperty

    @TaskAction
    fun verify() {
        val a = committed.get().asFile
        val b = regenerated.get().asFile
        val diffs = a.walkTopDown().filter { it.isFile }.mapNotNull { file ->
            val other = b.resolve(file.relativeTo(a).path)
            if (!other.exists() || other.readText() != file.readText()) file.path else null
        }.toList()
        if (diffs.isNotEmpty()) {
            throw GradleException(
                "Generated code is stale. Run ./gradlew generateDesignTokens generateTestIds " +
                "generateAnalytics generateDeeplinks and commit. Drifted:\n" +
                diffs.joinToString("\n") { "  - $it" }
            )
        }
    }
}
```

Wire the aggregate into the verification lifecycle:

```kotlin
val checkCodegenFresh = tasks.register("checkCodegenFresh") {
    dependsOn(generators.map { it.map { t -> t.name } })
    // in committed-output repos, run each generator with -Pcodegen.check
}
tasks.named("check").configure { dependsOn(checkCodegenFresh) }
```

### Determinism checklist for the generator

- Sort every emitted map/list by a stable key before writing.
- No timestamps, build numbers, machine names, or absolute paths in output.
- Fixed formatter (e.g. a pinned KotlinPoet version) and fixed indentation.
- Stable file naming; one deterministic file per logical group.
- Same JSON in, byte-identical Kotlin out, on any machine.

## Caching

```kotlin
@CacheableTask
abstract class GenerateFromSchemaTask : DefaultTask() {

    @get:InputFile
    @get:PathSensitive(PathSensitivity.RELATIVE)
    abstract val schema: RegularFileProperty

    @get:Classpath
    abstract val generatorClasspath: ConfigurableFileCollection

    @get:Input
    abstract val generatorVersion: Property<String>

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty
}
```

`@Classpath` on the generator artifact and `@Input` on its version mean a
generator upgrade invalidates the cached output; a schema edit invalidates via
`@InputFile`; an unchanged build is `UP-TO-DATE` or `FROM-CACHE`.

`gradle.properties`:

```properties
org.gradle.caching=true
org.gradle.configuration-cache=true
org.gradle.parallel=true
```

## Generated shapes

Illustrative target Kotlin per domain. Structure is the point; the specific
tokens, tags, events, and routes come from each repo's own schema.

### Design tokens (semantic data class, consumed by the Theme layer)

```kotlin
data class SemanticColors(
    val surface: Long,
    val onSurface: Long,
    val accent: Long,
)

val lightColors = SemanticColors(surface = 0xFFFFFFFF, onSurface = 0xFF1A1A1A, accent = 0xFF2962FF)
val darkColors  = SemanticColors(surface = 0xFF121212, onSurface = 0xFFEDEDED, accent = 0xFF82B1FF)

object Spacing { const val xs = 4; const val sm = 8; const val md = 16; const val lg = 24 }
```

### UI-test identifiers (one typed constant per tag)

```kotlin
object TestTags {
    const val PrimaryButton = "primary_button"
    const val EmailField = "email_field"
    const val ErrorBanner = "error_banner"
}

// UI:   Modifier.testTag(TestTags.PrimaryButton)
// Test: onNodeWithTag(TestTags.PrimaryButton).assertIsDisplayed()
```

### Analytics events (name plus typed parameters)

```kotlin
sealed interface AnalyticsEvent {
    val name: String
    val params: Map<String, String>

    data class ScreenViewed(val screen: String) : AnalyticsEvent {
        override val name = "screen_viewed"
        override val params get() = mapOf("screen" to screen)
    }

    data class ItemSelected(val itemId: String, val position: Int) : AnalyticsEvent {
        override val name = "item_selected"
        override val params get() = mapOf("item_id" to itemId, "position" to position.toString())
    }
}
```

### Deeplink manifest (routes plus typed parameters)

```kotlin
enum class Deeplink(val path: String) {
    Home("/home"),
    ItemDetail("/item/{id}"),
    Settings("/settings");

    companion object {
        fun match(path: String): Deeplink? = entries.firstOrNull { it.matches(path) }
    }

    private fun matches(candidate: String): Boolean =
        path.toRegex().matches(candidate) // generated from a stable path template
}
```
