$npx -y skills add skydoves/compose-performance-skills --skill migrating-to-modifier-nodeUse this skill to author new custom Jetpack Compose modifiers and migrate legacy ones from Modifier.composed { } to Modifier.Node + ModifierNodeElement<T>. Covers the persistent-node lifecycle (onAttach, onDetach, onReset, coroutineScope), the specialized node interfaces (DrawMod
| 1 | # Migrating to Modifier.Node — Persistent Nodes Over `composed { }` |
| 2 | |
| 3 | `Modifier.composed { }` allocates a fresh composable scope per modifier per composition: it cannot be skipped, cannot be hoisted, and forces the parent to run on every recomposition. `Modifier.Node` is a persistent node diffed by `ModifierNodeElement.equals()` — created once on first apply, updated in place on subsequent applies. There is **no per-recomposition allocation, no fresh composable scope, and no parent invalidation chain**, which is why Android Developers describes the system as "designed from the ground up to be far more performant" than the legacy `composed { }` factory (see `developer.android.com/develop/ui/compose/custom-modifiers`). This skill teaches Claude how to author new modifiers as `Modifier.Node` and migrate legacy `composed { }` factories. |
| 4 | |
| 5 | ## When to use this skill |
| 6 | |
| 7 | - A custom modifier currently uses `Modifier.composed { }` (search the module for `Modifier.composed`). |
| 8 | - Authoring a new custom modifier from scratch — never start with `composed { }`. |
| 9 | - A code review surfaces a `composed { }` factory. |
| 10 | - The custom modifier needs a `CoroutineScope` (animation loop, debouncer), reads a `CompositionLocal`, participates in layout / drawing / pointer input, or tracks layout coordinates. |
| 11 | - A `@TraceRecomposition` log shows the parent composable recomposing on every frame because a `composed { }` modifier is in the chain. |
| 12 | |
| 13 | ## When NOT to use this skill |
| 14 | |
| 15 | - The "modifier" is actually a one-line composable wrapper that can stay a `@Composable` function — leave it alone. |
| 16 | - The built-in modifier composition (`Modifier.padding(...).clickable(...)`) is sufficient, no custom node behavior needed. |
| 17 | - The fix the developer needs is reordering an existing chain, not authoring a new node — see `../ordering-modifier-chains/SKILL.md`. |
| 18 | - The custom modifier reads a hot animation value via `Modifier.composed { }` only to feed a value-form modifier underneath — the underlying issue is a wrong-phase state read; see `../../recomposition/deferring-state-reads/SKILL.md`. |
| 19 | |
| 20 | ## Prerequisites |
| 21 | |
| 22 | - **Compose UI 1.5+** — `Modifier.Node` and the specialized node interfaces are stable here. |
| 23 | - **Kotlin 2.0+** with `org.jetbrains.kotlin.plugin.compose` applied. Strong Skipping is on by default; non-skippable modifiers compound at scroll velocity. |
| 24 | - A release build for any final measurement (skydoves hot take #5: debug builds run interpreted and lie). |
| 25 | - For the full per-interface override surface and lifecycle diagram, read `references/modifier-node-anatomy.md` before authoring anything beyond a `DrawModifierNode`. |
| 26 | |
| 27 | ## Workflow |
| 28 | |
| 29 | - [ ] **1. Identify every `Modifier.composed { }` factory in the module.** Grep for `Modifier.composed`. Each match is one migration target. Note what the body does — `remember`, `drawBehind`, `LaunchedEffect`, a `CompositionLocal` read, a pointer handler — because that decides which specialized node interface(s) you need. |
| 30 | |
| 31 | - [ ] **2. Sketch the three pieces every Modifier.Node migration produces.** |
| 32 | - **(a) The public extension** — `fun Modifier.foo(...): Modifier = this then FooElement(...)`. Same name and signature as the old `composed { }` factory. |
| 33 | - **(b) The `ModifierNodeElement<T>` `data class`** — holds the parameters; implements `create()` (called once on first apply) and `update(node: T)` (called on subsequent applies). |
| 34 | - **(c) The `Modifier.Node` subclass** — holds mutable state (`var` fields), implements one or more specialized node interfaces, and runs lifecycle hooks (`onAttach`, `onDetach`, `onReset`). |
| 35 | |
| 36 | - [ ] **3. Make the Element a `data class`.** The compiler-synthesized `equals()`/`hashCode()` are how Compose decides whether to call `update()` vs leave the node alone. **MUST** be `data class`. A plain `cl |