Android Java to Kotlin Conversion (Second Pass)
The IDE converter produces Kotlin that compiles but is not idiomatic: platform types
everywhere, one giant function per lifecycle callback, nested if/else, new Thread,
switch, TextUtils.isEmpty, magic numbers. This skill drives the disciplined second
pass that turns that output into clean, modern, testable Kotlin — without changing
observable behaviour — and then writes a test that proves behaviour is unchanged.
The Two-Person Workflow
This skill assumes a hand-off:
Developer runs the mechanical IDE conversion (Code > Convert Java File to Kotlin File, or ⌥⇧⌘K), producing a .kt file that compiles but is not idiomatic.
You (Claude) drive everything after that:
Step 0 Baseline → Step 1 Idiomatic pass → Step 2 Behaviour-locking test → Step 3
Verify. If Step 3 fails or behaviour drifts, loop back to Step 1.
If you are handed a .java file instead, first apply a faithful 1:1 translation to reach
the same starting point, then continue.
The Prime Directive: Behaviour Must Not Change
Every transformation in this skill is behaviour-preserving. You are refactoring for
readability, safety, and modern API usage — not adding features or fixing bugs. If you
spot a real bug, surface it to the developer; do not silently "fix" it inside a
conversion. The behaviour-locking test in Step 3 exists to keep you honest.
Step 0: Establish Baseline
Before editing anything:
- Read the whole
.kt file (and, if you can, the original .java via
git show <rev>:<path>.java or the IDE's local history) to understand what it does.
- Write down the public/observable surface you must keep intact:
- Lifecycle callbacks (
onCreate, onViewCreated, onSaveInstanceState, …) and their
ordering of side-effects.
- Any Parcelable/Bundle keys, intent extras, and
newInstance(...) factory shapes.
- Note threading: which work runs off the main thread today (
Thread, AsyncTask,
runOnUiThread, executors) — this maps to coroutines in Step 2.
Step 1: Idiomatic Pass
Apply, in this order, then re-check the invariants:
- Kill platform types. Give every
! platform type an explicit nullable/non-null
type based on the Java source and call sites.
- Fail fast. Replace nested
if/else pyramids and null-checks with guard clauses
and require/requireNotNull/?: return. See FAIL-FAST.md.
- Decompose. Break each oversized lifecycle callback /
setupView-style method into
small, single-purpose private functions named for their intent. See
ANDROID-IDIOMS.md.
- Modern concurrency. Replace
new Thread, AsyncTask, runOnUiThread, and
callback pairs with lifecycleScope + suspend + withContext. See
CONCURRENCY.md.
- Modern Android + Kotlin idioms. Scope functions (
run/apply/let), extension
functions, when/partition/filter over switch, string templates, isNullOrEmpty,
view/KTX extensions. See ANDROID-IDIOMS.md. Retire
deprecated Android APIs (onActivityResult, options-menu overrides,
java.util.Observable) — see DEPRECATED-APIS.md.
- Project conventions. SPDX header, no magic numbers (
companion object +
const val), resources not hardcoded strings, ≤300 lines/file, ≤120 cols. See
PROJECT-CONVENTIONS.md.
- Testability seams. Extract pure logic (URL building, permission math, partitioning)
into functions you can unit-test; mark with
@VisibleForTesting where they must stay
internal/private-ish but be reachable from tests.
Do NOT expand scope. Unrelated files stay untouched (AGENTS.md / AI policy).
Step 2: Write a Behaviour-Locking Test (Mandatory)
A conversion is not complete until a test proves behaviour is unchanged. See
TESTING.md for the decision tree. In short:
- Pure functions you extracted (e.g. link builders,
isReshareForbidden,
partitioning) → fast JVM unit tests in app/src/test/ (JUnit4 + mockito-kotlin).
- Fragment/Activity/DB behaviour → instrumented test in
app/src/androidTest/
extending the project's base test class, or a Robolectric test where the project uses it.
- Prefer testing the seams you just created. If the Java original had no test, your
new test is the characterization test that locks current behaviour.
Every test file gets the SPDX header and follows the project's test conventions.
Step 3: Verify
Run and report real output — never claim green without evidence:
# Formatting + static analysis on the changed files
./gradlew spotlessKotlinCheck detektGplayDebug lintGplayDebug spotbugsGplayDebug
# Unit tests
./gradlew jacocoTestGplayDebugUnitTest
# Instrumented tests (if you wrote one), scoped to the class
./gradlew createGplayDebugCoverageReport -Pcoverage=true \
-Pandroid.testInstrumentationRunnerArguments.class=<fully.qualified.TestClass>
Worked Example
assets/worked-example.md is a real before/after from
FileDetailSharingFragment (871-line Java fragment → idiomatic Kotlin) showing each
transformation class in context. Read it when you need a concrete pattern.
Batch Conversion
Convert one file at a time, leaf dependencies first. Report progress per file. Warn the
developer before a change set grows into several thousand lines — split into focused PRs
(AGENTS.md). Each commit needs Assisted-by: <agent>:<model>; only the human adds
Signed-off-by.
1---2name: android-java-to-kotlin3description: Use when finishing a Java-to-Kotlin conversion in an Android project, when the user mentions "java to kotlin", "j2k", "convert java", "migrate java to kotlin", "finish the conversion", "make it idiomatic", or when a freshly IDE-converted .kt file needs to be turned into clean, modern, idiomatic Kotlin. The developer first runs the IDE converter (Android Studio: Code > Convert Java File to Kotlin File), then this skill drives the second pass: idiomatic cleanup, fail-fast control flow, coroutines/lifecycleScope, modern Android APIs, function decomposition, and a behaviour-locking test.4license: AGPL-3.0-or-later5---67# Android Java to Kotlin Conversion (Second Pass)89The IDE converter produces Kotlin that *compiles* but is not *idiomatic*: platform types10everywhere, one giant function per lifecycle callback, nested `if`/`else`, `new Thread`,11`switch`, `TextUtils.isEmpty`, magic numbers. This skill drives the disciplined second12pass that turns that output into clean, modern, testable Kotlin — **without changing13observable behaviour** — and then writes a test that proves behaviour is unchanged.1415## The Two-Person Workflow1617This skill assumes a hand-off:18191. **Developer** runs the mechanical IDE conversion (`Code > Convert Java File to Kotlin20 File`, or ⌥⇧⌘K), producing a `.kt` file that compiles but is not idiomatic.212. **You (Claude)** drive everything after that:2223 Step 0 Baseline → Step 1 Idiomatic pass → Step 2 Behaviour-locking test → Step 324 Verify. If Step 3 fails or behaviour drifts, loop back to Step 1.2526If you are handed a `.java` file instead, first apply a faithful 1:1 translation to reach27the same starting point, then continue.2829## The Prime Directive: Behaviour Must Not Change3031Every transformation in this skill is **behaviour-preserving**. You are refactoring for32readability, safety, and modern API usage — not adding features or fixing bugs. If you33spot a real bug, surface it to the developer; do not silently "fix" it inside a34conversion. The behaviour-locking test in Step 3 exists to keep you honest.3536## Step 0: Establish Baseline3738Before editing anything:39401. Read the whole `.kt` file (and, if you can, the original `.java` via41 `git show <rev>:<path>.java` or the IDE's local history) to understand *what it does*.422. Write down the **public/observable surface** you must keep intact:43 - Lifecycle callbacks (`onCreate`, `onViewCreated`, `onSaveInstanceState`, …) and their44 ordering of side-effects.45 - Any Parcelable/Bundle keys, intent extras, and `newInstance(...)` factory shapes.463. Note threading: which work runs off the main thread today (`Thread`, `AsyncTask`,47 `runOnUiThread`, executors) — this maps to coroutines in Step 2.4849## Step 1: Idiomatic Pass5051Apply, in this order, then re-check the invariants:52531. **Kill platform types.** Give every `!` platform type an explicit nullable/non-null54 type based on the Java source and call sites.552. **Fail fast.** Replace nested `if`/`else` pyramids and null-checks with guard clauses56 and `require`/`requireNotNull`/`?: return`. See [FAIL-FAST.md](references/FAIL-FAST.md).573. **Decompose.** Break each oversized lifecycle callback / `setupView`-style method into58 small, single-purpose private functions named for their intent. See59 [ANDROID-IDIOMS.md](references/ANDROID-IDIOMS.md).604. **Modern concurrency.** Replace `new Thread`, `AsyncTask`, `runOnUiThread`, and61 callback pairs with `lifecycleScope` + `suspend` + `withContext`. See62 [CONCURRENCY.md](references/CONCURRENCY.md).635. **Modern Android + Kotlin idioms.** Scope functions (`run`/`apply`/`let`), extension64 functions, `when`/`partition`/`filter` over `switch`, string templates, `isNullOrEmpty`,65 view/KTX extensions. See [ANDROID-IDIOMS.md](references/ANDROID-IDIOMS.md). Retire66 deprecated Android APIs (`onActivityResult`, options-menu overrides,67 `java.util.Observable`) — see [DEPRECATED-APIS.md](references/DEPRECATED-APIS.md).686. **Project conventions.** SPDX header, no magic numbers (`companion object` +69 `const val`), resources not hardcoded strings, ≤300 lines/file, ≤120 cols. See70 [PROJECT-CONVENTIONS.md](references/PROJECT-CONVENTIONS.md).717. **Testability seams.** Extract pure logic (URL building, permission math, partitioning)72 into functions you can unit-test; mark with `@VisibleForTesting` where they must stay73 `internal`/`private`-ish but be reachable from tests.7475Do NOT expand scope. Unrelated files stay untouched (AGENTS.md / AI policy).7677## Step 2: Write a Behaviour-Locking Test (Mandatory)7879A conversion is not complete until a test proves behaviour is unchanged. See80[TESTING.md](references/TESTING.md) for the decision tree. In short:8182- **Pure functions you extracted** (e.g. link builders, `isReshareForbidden`,83 partitioning) → fast JVM unit tests in `app/src/test/` (JUnit4 + mockito-kotlin).84- **Fragment/Activity/DB behaviour** → instrumented test in `app/src/androidTest/`85 extending the project's base test class, or a Robolectric test where the project uses it.86- Prefer testing the **seams you just created**. If the Java original had no test, your87 new test is the characterization test that locks current behaviour.8889Every test file gets the SPDX header and follows the project's test conventions.9091## Step 3: Verify9293Run and report real output — never claim green without evidence:9495```bash96# Formatting + static analysis on the changed files97./gradlew spotlessKotlinCheck detektGplayDebug lintGplayDebug spotbugsGplayDebug9899# Unit tests100./gradlew jacocoTestGplayDebugUnitTest101102# Instrumented tests (if you wrote one), scoped to the class103./gradlew createGplayDebugCoverageReport -Pcoverage=true \104 -Pandroid.testInstrumentationRunnerArguments.class=<fully.qualified.TestClass>105```106107## Worked Example108109[assets/worked-example.md](assets/worked-example.md) is a real before/after from110`FileDetailSharingFragment` (871-line Java fragment → idiomatic Kotlin) showing each111transformation class in context. Read it when you need a concrete pattern.112113## Batch Conversion114115Convert one file at a time, leaf dependencies first. Report progress per file. Warn the116developer before a change set grows into several thousand lines — split into focused PRs117(AGENTS.md). Each commit needs `Assisted-by: <agent>:<model>`; only the human adds118`Signed-off-by`.