kotlin-types-value-class
Use when writing or reviewing Kotlin type declarations to choose @JvmInline value class over data class where appropriate, including Compose stability implications.
What this skill does
# Kotlin value class vs data class
## Core principle
Prefer `@JvmInline value class` for single-field types that carry domain meaning. Data classes are for aggregating multiple fields. A value class gives you type safety (you can't mix up `UserId` and `String`) without the allocation overhead of a data class.
## When to use this skill
- Writing a new Kotlin type that wraps a single value
- Reviewing a data class that has only one property
- Seeing primitive types (`String`, `Long`, `Int`, etc.) used where a domain type would prevent misuse
- Compose compiler reports showing unstable parameters that could be value classes
## Decision flow
| Situation | Prefer |
|---|---|
| Single field + domain-meaningful (`UserId`, `EmailAddress`, `Percentage`) | `@JvmInline value class` |
| Single field + no domain meaning (just grouping) | Type alias or keep the primitive |
| Multiple fields | Data class |
| Needs custom `equals`/`hashCode` beyond the wrapped value | Data class (value classes delegate to the underlying type) |
| Used as a generic type argument or nullable in hot paths | Data class or primitive (autoboxing cost) |
```kotlin
// GOOD: domain-meaningful single field
@JvmInline value class UserId(val value: String)
@JvmInline value class EmailAddress(val value: String)
@JvmInline value class Percentage(val value: Float)
// BAD: data class wrapping a single field
data class UserId(val value: String) // unnecessary allocation
data class EmailAddress(val value: String) // type safety without the overhead is available
// BAD: value class with no domain meaning
@JvmInline value class Wrapper(val value: String) // just use the String, or a type alias
// BAD: value class needing custom equality
@JvmInline value class CaseInsensitiveString(val value: String)
// value class equals delegates to String equals, which IS case-sensitive
// Use a data class if you need different equality semantics
```
## Compose stability
`@JvmInline value class` is treated as `Stable` by the Compose compiler when its underlying type is stable (primitives, `String`, and other stable types). This means:
- Value classes passed as composable parameters avoid "unstable parameter" warnings
- No need for `@Immutable` annotations at Compose boundaries when wrapping primitives or strings
- Replacing single-field data classes with value classes at UI boundaries improves skippability
```kotlin
// Before: data class wrapping a single field
data class UiState(val userId: String) // works, but allocates a wrapper object
// After: value class is stable and zero-allocation at runtime
@JvmInline value class UserId(val value: String)
data class UiState(val userId: UserId)
```
## Gotchas
- **Autoboxing**: Value classes are unboxed at compile time but boxed (allocated) when used as nullable (`UserId?`), generic type arguments (`List<UserId>`), or vararg parameters. In hot paths these allocations matter; in most code they don't.
- **No backing fields**: You cannot use `init` blocks, `lateinit`, or delegated properties like `by lazy`. The class body is extremely constrained — only the single constructor parameter exists.
- **No data-class conveniences**: No `copy()`, no `component1()` for destructuring. If you need these, use a data class. You *can* override `toString()` in a value class, but the default is `ClassName(fieldName=value)` — it does not delegate to the underlying type's `toString()`. Override it yourself if you need a different representation.
- **No custom equals/hashCode**: These always delegate to the underlying type. Need custom equality → use a data class.
- **when exhaustiveness**: Sealed hierarchies of value classes work differently than data class hierarchies. Test `when` branches carefully.
- **Serialization semantics**: With kotlinx.serialization, a `@Serializable data class A(val value: String)` serializes as `{"value":"..."}`, but a `@Serializable value class A(val value: String)` serializes as the underlying value (`"..."`). Replacing a single-field data class with a value class is a breaking change for your API/JSON contract.
- **Serialization**: Some serialization frameworks need explicit support for value classes (e.g., kotlinx.serialization's `@Serializable` works, but Jackson may need configuration).
- **Interoperability**: From Java, value classes appear as their underlying type. Java callers bypass the type-safety wrapper.
- **Reflection and runtime erasure**: When passed as `Any` or used in generic contexts, value classes box into a synthetic wrapper class. Java reflection sees mangled method signatures, and frameworks that rely on raw runtime types (some ORMs, DI containers, or serializers) may see the underlying type rather than the value class.
## Packing multiple values
A value class can only declare one field, but Compose provides `packFloats`, `packInts`, and matching `unpack*` functions in `androidx.compose.ui.util` to store multiple primitives in a single `Long`. This lets you represent composite values (e.g., a 2D point, size, or padding) as a zero-allocation value class instead of a multi-field data class.
```kotlin
@JvmInline value class Offset(val packedValue: Long)
fun Offset(x: Float, y: Float): Offset = Offset(packFloats(x, y))
val Offset.x: Float get() = unpackFloat1(packedValue)
val Offset.y: Float get() = unpackFloat2(packedValue)
```
- **Only use this in performance-critical paths** — manual bit-packing is error-prone. A data class is simpler and safer for most UI types.
- **Available in `androidx.compose.ui.util`** — `packFloats`, `packInts`, `unpackFloat1`, `unpackFloat2`, `unpackInt1`, `unpackInt2`.
## Common mistakes
| Mistake | Fix |
|---|---|
| Data class wrapping a single domain field | Replace with `@JvmInline value class` |
| Value class with no domain meaning (just a wrapper) | Use a type alias or the primitive directly |
| Value class needing custom equality | Use a data class instead |
| Value class as generic type argument in hot path | Accept autoboxing cost or use the primitive |
| `@Immutable` annotation on a type that could be a value class | Replace with value class — it's Stable by default |
| Forgetting `@JvmInline` annotation | Always pair `value class` with `@JvmInline` for single-field classes |
## Red flags during review
- A data class with exactly one property
- A `String`, `Long`, or `Int` used where different values should not be interchangeable (e.g., `fun transfer(from: String, to: String, amount: Long)`)
- An `@Immutable` annotation on a single-field wrapper
- A type alias used for domain distinction where value-class semantics are needed (type aliases are type-erased, no runtime protection)
## When NOT to apply
- The type needs multiple fields → data class
- The type needs custom `equals`/`hashCode` → data class
- The type is used heavily as a nullable or generic in performance-critical code → measure autoboxing cost first
- The project does not need the type-safety distinction → a type alias or primitive is sufficient
## Related
- [`compose-stability-diagnostics`](../compose-stability-diagnostics/SKILL.md) — diagnose unstable Compose parameters; value classes are one fix
Related in Writing & Docs
jax-development
IncludedUse this skill when the user is writing, debugging, profiling, refactoring, reviewing, benchmarking, parallelising, exporting, or explaining JAX code, or when they mention JAX, jax.numpy, jit, grad, value_and_grad, vmap, scan, lax, random keys, pytrees, jax.Array, sharding, Mesh, PartitionSpec, NamedSharding, pmap, shard_map, Pallas, XLA, StableHLO, checkify, profiler, or the JAX repo. It helps turn NumPy or PyTorch-style code into pure functional JAX, fix tracer/control-flow/shape/PRNG bugs, remove recompiles and host-device syncs, choose transforms and sharding strategies, inspect jaxpr/lowering/IR, and benchmark compiled code correctly.
nature-article-writer
IncludedDrafts, rewrites, diagnostically critiques, and style-calibrates primary research manuscripts for Nature and Nature Portfolio journals. Use when the user wants a Nature-style title, summary paragraph or abstract, introduction, results, discussion, methods, figure legends, presubmission enquiry, cover letter, reviewer response, or when a scientific draft sounds generic, jargon-heavy, structurally weak, or AI-ish and needs precise, broad-reader-friendly prose without inventing data, analyses, or references. Best for primary research articles and letters rather than reviews or press releases unless explicitly adapting one.
deckrd
IncludedDocument-driven framework that derives requirements, specifications, implementation plans, and executable tasks from goals through structured AI dialogue. Use when user says "write requirements", "create spec", "plan implementation", "derive tasks", "structure this feature", "break down into tasks", or "document this module". Also use for reverse engineering existing code into docs (/deckrd rev). Do NOT use for direct code writing — use /deckrd-coder after tasks are generated. Do NOT use when the user only wants to run or fix existing code without planning.
clinical-decision-support
IncludedGenerate professional clinical decision support (CDS) documents for pharmaceutical and clinical research settings, including patient cohort analyses (biomarker-stratified with outcomes) and treatment recommendation reports (evidence-based guidelines with decision algorithms). Supports GRADE evidence grading, statistical analysis (hazard ratios, survival curves, waterfall plots), biomarker integration, and regulatory compliance. Outputs publication-ready LaTeX/PDF format optimized for drug development, clinical research, and evidence synthesis.
handling-sf-data
IncludedSalesforce data operations with 130-point scoring. Use this skill to create, update, delete, bulk import/export, generate test data, and clean up org records using sf CLI and anonymous Apex. TRIGGER when: user creates test data, performs bulk import/export, uses sf data CLI commands, needs data factory patterns for Apex tests, or needs to seed/clean records in a Salesforce org. DO NOT TRIGGER when: SOQL query writing only (use querying-soql), Apex test execution (use running-apex-tests), or metadata deployment (use deploying-metadata).
accelint-ac-to-playwright
IncludedConvert and validate acceptance criteria for Playwright test automation. Use when user asks to (1) review/evaluate/check if AC are ready for automation, (2) assess if AC can be converted as-is, (3) validate AC quality for Playwright, (4) turn AC into tests, (5) generate tests from acceptance criteria, (6) convert .md bullets or .feature Gherkin files to Playwright specs, (7) create test automation from requirements. Handles both bullet-style markdown and Gherkin syntax with JSON test plan generation and validation.