# Ha Android Kotlin Style

> Home Assistant Android Kotlin style. Use when declaring constants, modeling data or logic types, working with time or dates, choosing visibility, naming, or writing KDoc and comments.

- Skill: `home-assistant/ha-android-kotlin-style` (Agent Skill)
- Install (CLI): `npx skillmds@latest add home-assistant/ha-android-kotlin-style`
- Raw SKILL.md: https://api.skillmd.com/api/skills/home-assistant/ha-android-kotlin-style/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: home-assistant (https://skillmd.com/u/home-assistant)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/home-assistant/ha-android-kotlin-style

---


# HA Android Kotlin Style

Use this skill when writing or reviewing Kotlin code style: constants, types, immutability, time, visibility, and documentation.

KTLint enforces formatting via `.editorconfig`; format with `./gradlew :build-logic:convention:ktlintFormat ktlintFormat`. Style rationale lives at https://developers.home-assistant.io/docs/android/codestyle and https://developers.home-assistant.io/docs/android/best_practices.

## Constants

Replace magic numbers and strings with named constants, placed near their usage:

```kotlin
// Good - private, at file level, outside any companion object
private const val MAX_RETRIES = 3

// For constants shared across a module, a dedicated *Constants.kt file with object namespacing
object NetworkConstants {
    const val MAX_RETRIES = 3
}
```

Avoid `companion object` for constants unless they must be accessed externally under the class name or several classes in one file need the same constant name. Use `@VisibleForTesting` when a constant is only exposed for tests.

## Strong Types, Never Strings for Logic

- Use sealed classes/interfaces over enums or strings for logic control. Sealed subtypes can carry their own data, and `when` without an `else` branch makes the compiler catch missing cases.
- Reserve strings for raw third-party values or UI display; wrap unavoidable strings in a strong type such as an inline value class. Avoid string manipulation to build structured values (URLs, identifiers) — work with the typed object (`Url`, a value class) and convert at the boundary.
- Never use `Pair`/`Triple` in an API — a caller can't tell what the components mean. Define a small data class with named fields instead.
- Use Kotlin `Duration`/`Instant` instead of primitive types for time. If a primitive is unavoidable, put the unit in the name (`THRESHOLD_MS`), and convert to a strong type as early as possible. Note that `Duration` is not a compile-time constant: `val STOP_TIMEOUT = 500.milliseconds`, not `const val`.
- Locale-sensitive operations like `lowercase()` must not feed logic: for sort keys, identifiers, and search matching use `lowercase(Locale.ROOT)`, otherwise matching breaks under locales like Turkish (dotted/dotless I).
- Never use `System.currentTimeMillis`; use `kotlin.time.Clock` injected through Hilt, and expose the clock as a parameter or attribute so tests can control it.

## Immutability and Functions

- Store data in immutable classes exposing `copy()` or functions returning a new modified instance.
- Keep functions small (under 50 lines), single-responsibility, with meaningful names. If a function is hard to name, it does too much.
- Prefer composition over inheritance.

## Keep It Direct

- Don't introduce an abstraction (interface, seam, wrapper) for a single implementation or a hypothetical future need — use the proven library or API directly. Abstract only when a second real use appears, or to hide an implementation from outside its module (public interface, `internal` implementation bound through DI) — see `ha-android-architecture` for the interface + `*Impl` criteria.
- Don't store what can be derived: a field duplicating a map key, an ID rebuildable from its parts, or a flag mirroring other state is a bug waiting for a desync.
- When calling a function with several parameters of the same or primitive types, use named parameters instead of relying on order.

## Visibility

Default to the narrowest visibility: private to the file or class. Before making anything public, check whether `internal` suffices.

## Imports

When adding an import, add the import and its usage in the same edit — linters and hooks strip unused imports, so an import added in a separate step gets removed. The reverse also bites: after removing or moving code, delete the imports that became unused — ktlint fails CI on them.

## Documentation and Comments

- Every public function needs KDoc focused on the functionality, not the implementation. Mention implementation details only when they affect usage (synchronization constraints, what it throws).
- Keep it to the essential and straight to the point: state the contract in as few words as it takes and stop. Don't narrate the implementation, restate the obvious from the signature, explain rationale that belongs in a commit or issue, or pad with AI-sounding prose — a one-line function usually needs a one-line doc.
- Keep KDoc in sync with the code: when changing what a function does, targets, or emits, update its documentation in the same change — stale KDoc naming the wrong class or error type misleads the next reader.
- Explain the "why", not just the "what". Write for developers who may not be native English speakers: no abbreviations, add references when useful.
- Avoid TODOs; if one is required, link an existing GitHub issue: `// TODO Missing feature https://github.com/home-assistant/android/issues/404`. Don't invent an issue number or open a new issue yourself — ask the user for the issue, or leave the TODO out.
- GitHub references in comments and KDoc are always full HTTP links, never bare `#1123` — a number is ambiguous and unclickable outside GitHub. When the reference points at code (a file, line, or commit), use a permalink (commit-SHA URL, `y` on GitHub) so the link survives file moves and edits.

