Kotlin compiler plugin · Android + iOS

Change the number.
See the result.

Mark ordinary Kotlin values or an enum class with @DebugInput. DebugInput generates a type-safe tuning page, persists tester changes, and exports every changed default as JSON.

vlatest current release Kotlin 2.4.20 exact compiler line 23 supported type shapes
Animation.kt
/** Playback multiplier used by previews. */
@DebugInput
val playbackSpeed = 1.0
ANIMATION playbackSpeed kotlin.Double
1.35
Changed
{ } Copy JSON (1)
No manual registryDescriptors are generated from source.
KDoc is the copyDocumentation stays beside the value.
Release-safeProduction reads always return defaults.
PersistentOverrides survive app relaunches.

Two repositories, one plugin

Gradle resolves plugins and libraries from separate repository lists. Add the public DebugInput Maven repository to both lists in the root settings.gradle.kts.

Compiler compatibility

DebugInput latest requires Kotlin 2.4.20. The Gradle plugin fails early with a readable error when the consumer uses a different Kotlin version.

settings.gradle.kts
pluginManagement {
    repositories {
        google()
        maven("https://maven.rohittp.com")
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositories {
        google()
        maven("https://maven.rohittp.com")
        mavenCentral()
    }
}

Apply the plugin to every Kotlin module that declares debug inputs, and to the module that hosts DebugInputsPage() so its call site can aggregate those inputs:

shared/build.gradle.kts
plugins {
    id("com.rohittp.debug-input") version "latest"
}

Add the optional Compose UI artifact in that page-hosting module:

app/build.gradle.kts
dependencies {
    implementation("com.rohittp:debug-input-compose:latest")
}
No runtime dependency declaration

The Gradle plugin adds debug-input-runtime to instrumented source sets automatically. The Compose artifact exposes it transitively to the UI host.

Annotate, host, tune

1Annotate a val

The declaration keeps its normal Kotlin type and default.

2Host the page

Add one composable to your existing debug settings screen.

3Edit on-device

Reads resolve the persisted override until it is reset.

Animation.kt
import com.rohittp.debuginput.DebugInput

/** Multiplier applied to preview playback. */
@DebugInput
val playbackSpeed: Double = 1.0
DebugSettings.kt
import com.rohittp.debuginput.compose.DebugInputsPage

@Composable
fun DebugSettings() {
    DebugInputsPage()
}

The compiler rewrites the call site so DebugInputsPage() receives every generated descriptor reachable from that module. There is no list to maintain.

Keep tuning values where they belong

Annotate top-level or object properties. The property's KDoc becomes its help text, and its declaring file, class, or object becomes the default section.

Physics.kt
/**
 * Keeps the camera inside the route bounds
 * during fast preview scrubbing.
 */
@DebugInput
val cameraPadding = 48.0

/** Draw route labels in preview. */
@DebugInput
val showLabels = true
Generated section

Physics

cameraPadding48.0
showLabelstrue

Explicit metadata is optional

Use docs only when the UI needs different prose from source readers. Use section to merge related values from different files into one page.

Explicit overrides
@DebugInput(
    section = "Animation tuning",
    docs = "Tester-facing explanation shown in the app.",
)
val trailOpacity = 0.72

Turn a constants table into one tuning page

Annotating an enum class instruments every supported constructor val on every constant. This works well for large, documented tuning tables.

MagicNumbers.kt
/** Values controlling route animation and rendering. */
@DebugInput(section = "Animation constants")
enum class MagicNumbers(vararg val values: Double) {
    /** Widths for low, normal, and high emphasis. */
    ROUTE_LINE_WIDTHS(2.0, 4.0, 6.0),

    /** Scale ladder used by the flag marker. */
    FLAG_SCALES(0.6, 0.8, 1.0),
}
One constructor val

Rows are named ROUTE_LINE_WIDTHS; the redundant property suffix is omitted.

Multiple constructor vals

Rows are named FREE.limit, FREE.label, and so on.

Stable hot path

Enum getters use an ordinal-indexed id table. No string concatenation happens per read.

Entry bodies work

Constants with anonymous bodies still inherit the ordinal used by the generated table.

Override one entry's documentation

Enum entry metadata
@DebugInput(docs = "Shorter tester-facing explanation.")
MINIMUM_DURATION_CEILING_SEC(8.0)

The class-level annotation creates the inputs. An annotation on one entry only overrides that entry's documentation.

Source structure becomes navigation

Every compiler-generated section is a root card leading to a dedicated subpage. The default title is the declaring file, class, object, or enum name. An explicit section title groups ordinary values with the same title inside a module.

DeclarationDefault sectionDescription
Top-level propertySource file nameProperty KDoc
Class or object propertyClass or object nameProperty KDoc
Annotated enum classEnum nameEnum class KDoc
Explicit sectionProvided titleDeclaration KDoc

KDoc precedence

  1. 1
    Explicit docs

    Use when the in-app explanation intentionally differs from source documentation.

  2. 2
    Declaration KDoc

    The normal path for values, enum classes, and enum entries.

  3. 3
    Empty

    The row remains usable without an information action.

KDoc normalization

Block tags such as @param and @see are dropped, qualified KDoc links are shortened to their final name, star margins are collapsed, and paragraph breaks are retained. The full body is kept.

A settings page that scales from one value to hundreds

DebugInputsPage() inherits the host app's Material theme and fills its bounded parent. Sections become subpages, keeping the root readable, and a search bar on every page finds any input by name.

01
Type-aware editors

Validated number and text fields, Boolean controls, enum selection, and element editors for composites.

02
Clear change state

Changed badges, per-row reset, changed counts on sections, and one global reset.

03
Documentation in context

Property or enum-entry KDoc is available beside the value; enum KDoc introduces its section page.

04
Module-aware aggregation

Inputs from dependent modules are collected, de-duplicated by id, and sorted deterministically.

05
Search across pages

A search bar on every page matches names and ids. From the root it reaches into section pages and opens the page at the matching input; inside a section it filters that page.

Search

Every page has a search bar at the top. It matches an input's display name or its stable id, ignoring case, so both speed and app.physics find com.app.physics.speed. What the results look like depends on where you search:

Searching fromCoversA match shows as
The root pageEvery input, including those on section pagesAn editable row when the input sits on the root; otherwise a result naming its section. Tapping the result opens that section page, scrolled to the input and outlined.
A section pageThat page's inputs onlyThe input's editable row; the rest of the page is filtered out.

Back from a section page returns to the root with the same query and results. Clearing the field restores the full page. Search needs no setup and no API: it is part of DebugInputsPage() from 0.2.0.

Host responsibility

The composable does not install a MaterialTheme or its own navigation destination. Put it inside your existing debug-only settings flow and give it bounded height.

Copy changed defaults as JSON

The page exports only effective overrides that differ from the declaration's current default. The payload is stable-id sorted, versioned, and copied directly to the clipboard so testers can send it to developers.

Clipboard payload
{
  "version": 1,
  "changes": [
    {
      "id": "com.app.Animation.playbackSpeed",
      "module": ":shared",
      "section": "Animation",
      "name": "playbackSpeed",
      "type": "kotlin.Double",
      "default": 1.0,
      "value": 1.35
    }
  ]
}

Dormant overrides from renamed, removed, or type-changed inputs are excluded. Arrays and composites are exported as JSON arrays; enum values and characters are strings.

Built-in editors for common Kotlin shapes

Scalars

ByteShortIntLongFloatDoubleBooleanCharString

Choice

Enum

Collections

List<T>Set<T>Array<T>

Primitive arrays

ByteArrayShortArrayIntArrayLongArrayFloatArrayDoubleArrayBooleanArrayCharArray

Tuples

Pair<A, B>Triple<A, B, C>

Collection elements and tuple slots must use supported scalar shapes. Nested containers, maps, and enums inside containers are rejected by design.

Generated at compile time, cheap at read time

Source@DebugInput val
CompilerGetter + descriptor
RuntimeOverride registry
UIEdit + export

Read path

A generated getter calls a type-specific resolver. Scalar reads use a debug-build check, volatile map read, and lookup. Enum-class inputs load their stable id from an ordinal table.

Persistence

Android uses dependency-free automatic initialization plus SharedPreferences. iOS uses an isolated NSUserDefaults suite. Hydration is lazy on first read.

Cross-module pages

Each instrumented module publishes a descriptor manifest. The consuming page call aggregates direct dependency descriptors hierarchically.

Stable identity

Property ids derive from package and declaration identity. Section display titles are separate from page identity, so equal file names do not collide.

Defaults remain authoritative in release builds

On Android application release compilations, the transform is disabled. For Kotlin Multiplatform Android libraries, AGP exposes one Android variant, so generated calls can remain in the artifact; the runtime debug check still guarantees defaults. On iOS, debug/release identity is resolved at final link time.

Do not use tester JSON as runtime configuration

The export is a developer handoff. Review it, update the Kotlin declarations, and let those new source defaults ship through the normal production process.

Know what is deliberately outside the line

  • ×
    Exact Kotlin pin

    The compiler plugin currently supports Kotlin 2.4.20, not a range.

  • ×
    Ordinary instance properties

    An annotated property on an ordinary class instance has no statically reachable descriptor default. Prefer top-level, object, or enum-class inputs.

  • ×
    No custom renderers yet

    Unsupported type shapes render no editor. Maps and nested containers are rejected.

  • ×
    No iOS x64 Compose page

    The runtime supports iOS x64, but the Compose dependency line no longer publishes that simulator target.

  • ×
    Debug tuning is not synchronized

    Overrides are local to one app installation. Use Copy JSON for collaboration.

Common integration failures

Plugin com.rohittp.debug-input was not found

Add https://maven.rohittp.com inside pluginManagement.repositories in the root settings file. A dependency repository alone cannot resolve a Gradle plugin marker.

debug-input-runtime or Compose cannot be resolved

Add the same repository inside dependencyResolutionManagement.repositories. Plugin and library resolution are separate.

The build reports a Kotlin version mismatch

Use Kotlin 2.4.20 for DebugInput latest. This guard prevents binary-incompatible compiler internals from failing later with obscure FIR or IR errors.

Which DebugInput version matches my Kotlin version?

Each release line is built for exactly one Kotlin minor: 0.2.x for Kotlin 2.4.x (built on 2.4.20) and 0.1.x for Kotlin 2.3.x (built on 2.3.21). A project still on Kotlin 2.3 should stay on 0.1.0; upgrading to 0.2.0 needs no source changes beyond the Kotlin bump.

The page is empty

Confirm the plugin is applied to the module that declares inputs and that DebugInputsPage() is compiled in an instrumented module. An empty page is expected in release builds.

A value resets after editing

Invalid text is not stored. For composites, every element must match the declared shape. A stored override from an older type is dormant and the current declaration resolves its default.

Two sections have the same title

Default sections keep separate internal page identities even when two files share a name. If they are conceptually one page, assign the same explicit section title.

Small surface, generated behavior

@DebugInput(docs = "", section = "")

Marks a property or enum class. Both parameters are optional and fall back to KDoc and source structure.

DebugInputsPage(descriptors = emptyList(), modifier = Modifier)

Renders the generated debug input catalogue. Normal source calls omit descriptors; the compiler supplies them.

Registry and descriptor internals are intentionally marked internal opt-in API. Build integrations should depend on the annotation, Gradle plugin, and page rather than on generated function names or codec details.