Compose Performance Audit
Moustachauve/WLED-Android
Audit and improve Jetpack Compose runtime performance from code review and architecture.
A skill your agent uses to author new custom Jetpack Compose modifiers and migrate legacy ones from Modifier.composed { } to Modifier.Node + ModifierNodeElement<T.
$ npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install rosuH/EasyWatermark migrating-to-modifier-node --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/rosuH/EasyWatermark.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/migrating-to-modifier-node .claude/skills/migrating-to-modifier-node && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "migrating-to-modifier-node" agent skill from https://github.com/rosuH/EasyWatermark/tree/master/.agents/skills/migrating-to-modifier-node into .claude/skills/migrating-to-modifier-node/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrating-to-modifier-node", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/rosuH/EasyWatermark/tree/master/.agents/skills/migrating-to-modifier-nodeType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install rosuH/EasyWatermark migrating-to-modifier-node --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/rosuH/EasyWatermark.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/migrating-to-modifier-node .agents/skills/migrating-to-modifier-node && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "migrating-to-modifier-node" agent skill from https://github.com/rosuH/EasyWatermark/tree/master/.agents/skills/migrating-to-modifier-node into .agents/skills/migrating-to-modifier-node/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrating-to-modifier-node", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install rosuH/EasyWatermark migrating-to-modifier-node --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/rosuH/EasyWatermark.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/migrating-to-modifier-node .cursor/skills/migrating-to-modifier-node && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "migrating-to-modifier-node" agent skill from https://github.com/rosuH/EasyWatermark/tree/master/.agents/skills/migrating-to-modifier-node into .cursor/skills/migrating-to-modifier-node/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrating-to-modifier-node", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/rosuH/EasyWatermark.git --path .agents/skills/migrating-to-modifier-node--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install rosuH/EasyWatermark migrating-to-modifier-node --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/rosuH/EasyWatermark.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/migrating-to-modifier-node .gemini/skills/migrating-to-modifier-node && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "migrating-to-modifier-node" agent skill from https://github.com/rosuH/EasyWatermark/tree/master/.agents/skills/migrating-to-modifier-node into .gemini/skills/migrating-to-modifier-node/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrating-to-modifier-node", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install rosuH/EasyWatermark migrating-to-modifier-nodeInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/rosuH/EasyWatermark.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/migrating-to-modifier-node .github/skills/migrating-to-modifier-node && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "migrating-to-modifier-node" agent skill from https://github.com/rosuH/EasyWatermark/tree/master/.agents/skills/migrating-to-modifier-node into .github/skills/migrating-to-modifier-node/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrating-to-modifier-node", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install rosuH/EasyWatermark migrating-to-modifier-node --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/rosuH/EasyWatermark.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/migrating-to-modifier-node .opencode/skills/migrating-to-modifier-node && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "migrating-to-modifier-node" agent skill from https://github.com/rosuH/EasyWatermark/tree/master/.agents/skills/migrating-to-modifier-node into .opencode/skills/migrating-to-modifier-node/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "migrating-to-modifier-node", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
migrating-to-modifier-nodeA skill your agent uses to author new custom Jetpack Compose modifiers and migrate legacy ones from Modifier.composed { } to Modifier.Node + ModifierNodeElement<T.
Migrating To Modifier Node is an agent skill from rosuH/EasyWatermark. Use 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 (DrawModifierNode, LayoutModifierNode, SemanticsModifierNode, PointerInputModifierNode, CompositionLocalConsumerModifierNode, LayoutAwareModifierNode, GlobalPositionAwareModifierNode, ObserverModifierNode, DelegatingNode, TraversableNode), why…
Its SKILL.md is about 5k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files, including reference files (for example `references/modifier-node-anatomy.md`).
It sits in Mobile, covering Android development. It works with Jetpack Compose. The repository describes itself as: 🔒 🖼 Securely, easily add a watermark to your sensitive photos. 安全、简单地为你的敏感照片添加水印,防止被人泄露、利用. The licence is Apache-2.0.
Read from SKILL.md and the folder at commit 61223db. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are kotlin).
From the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
developer.android.commedium.comchrisbanes.mespeakerdeck.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Migrating To Modifier Node loads about 5k tokens when it runs, and up to ~8.5k if it reads all its reference files. Until then it costs about 229 tokens; SKILL.md has 1,355 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from rosuH/EasyWatermark at commit 61223db, republished under its Apache-2.0 licence (© rosuH). 1,355 words, ~4,952 tokens.
.claude/skills/migrating-to-modifier-node/SKILL.md (or your agent's skills folder). This skill also uses 1 other file; get the full folder from GitHub.composed { }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.
Modifier.composed { } (search the module for Modifier.composed).composed { }.composed { } factory.CoroutineScope (animation loop, debouncer), reads a CompositionLocal, participates in layout / drawing / pointer input, or tracks layout coordinates.@TraceRecomposition log shows the parent composable recomposing on every frame because a composed { } modifier is in the chain.@Composable function — leave it alone.Modifier.padding(...).clickable(...)) is sufficient, no custom node behavior needed.../ordering-modifier-chains/SKILL.md.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.Modifier.Node and the specialized node interfaces are stable here.org.jetbrains.kotlin.plugin.compose applied. Strong Skipping is on by default; non-skippable modifiers compound at scroll velocity.references/modifier-node-anatomy.md before authoring anything beyond a DrawModifierNode. 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.
2. Sketch the three pieces every Modifier.Node migration produces.
fun Modifier.foo(...): Modifier = this then FooElement(...). Same name and signature as the old composed { } factory.ModifierNodeElement<T> data class — holds the parameters; implements create() (called once on first apply) and update(node: T) (called on subsequent applies).Modifier.Node subclass — holds mutable state (var fields), implements one or more specialized node interfaces, and runs lifecycle hooks (onAttach, onDetach, onReset). 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 class falls back to referential equality, the diff thinks every apply is a new modifier, and update() is never called — your node holds stale parameters silently.
4. Pick the right specialized node interface(s). A Modifier.Node is empty by itself; behavior comes from interfaces it implements. The common ones:
| Interface | Use when the modifier needs to … |
|---|---|
DrawModifierNode | draw (replaces drawBehind/drawWithCache) |
LayoutModifierNode | measure/place (replaces layout { } and custom Layout) |
SemanticsModifierNode | contribute to accessibility |
PointerInputModifierNode | handle pointer / gesture input (replaces pointerInput) |
CompositionLocalConsumerModifierNode | read a CompositionLocal from inside the node |
LayoutAwareModifierNode | get notified when this node's size/coordinates change |
GlobalPositionAwareModifierNode | get notified about position in the window/root |
ObserverModifierNode | observe arbitrary state reads with a custom observer (observeReads { ... }) |
DelegatingNode | compose multiple node behaviors by delegating to child nodes |
TraversableNode | walk the modifier chain (parent/child traversal) |
A node MAY implement several interfaces at once — e.g. DrawModifierNode + CompositionLocalConsumerModifierNode + LayoutAwareModifierNode. For complex multi-behavior modifiers, PREFERRED: compose smaller DelegatingNode children rather than one mega-node implementing five interfaces.
5. Use the built-in coroutineScope for async work. Every Modifier.Node exposes a coroutineScope: CoroutineScope lazily tied to the node's attach/detach lifecycle. Launch animations, observers, debouncers there from onAttach(). MUST NOT create your own CoroutineScope inside onAttach — you will leak it past onDetach.
6. Trigger re-runs explicitly when needed. When you mutate node state from inside update() or a coroutine and need a redraw / re-measure / re-place, call invalidateDraw(), invalidateMeasurement(), or invalidatePlacement(). By default, update() triggers an auto-invalidation; for fine control, override shouldAutoInvalidate = false and invalidate manually.
7. Implement onAttach/onDetach/onReset for resource lifecycle. onAttach runs when the node joins the tree; onDetach when it leaves; onReset when the node is reused (only relevant inside lazy layouts). MUST release listeners, observers, and external subscriptions in onDetach.
8. Verify migration: no Modifier.composed { } remains. Re-grep the module. If any composed { } calls survive, list them with rationale; otherwise the migration is complete.
composed { drawBehind } to DrawModifierNode// WRONG (legacy)
fun Modifier.circle(color: Color): Modifier = composed {
val computed = remember(color) { color.copy(alpha = 0.5f) }
drawBehind { drawCircle(computed) }
}
// WRONG because: composed { } opens a fresh composable scope per parent recomposition; the modifier can never be skipped and forces the parent to recompose on every read it does inside.// RIGHT
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
override fun create(): CircleNode = CircleNode(color)
override fun update(node: CircleNode) { node.color = color }
}
private class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
override fun ContentDrawScope.draw() {
drawCircle(color.copy(alpha = 0.5f))
drawContent()
}
}
fun Modifier.circle(color: Color): Modifier = this then CircleElement(color)The data class Element gives equals() / hashCode() for free. When the caller passes the same color, equals() returns true and the node is left alone. When the color changes, update() mutates node.color in place — no allocation, no Composition invalidation in the parent.
composed { LaunchedEffect })// WRONG (legacy)
fun Modifier.pulse(period: Long): Modifier = composed {
val alpha = remember { Animatable(1f) }
LaunchedEffect(period) {
while (true) { alpha.animateTo(0.3f); alpha.animateTo(1f); delay(period) }
}
graphicsLayer { this.alpha = alpha.value }
}
// WRONG because: every parent recomposition allocates a new composable scope, and the LaunchedEffect's keying logic re-evaluates inside that scope.// RIGHT
private data class PulseElement(val period: Long) : ModifierNodeElement<PulseNode>() {
override fun create(): PulseNode = PulseNode(period)
override fun update(node: PulseNode) { node.period = period }
}
private class PulseNode(var period: Long) : Modifier.Node(), DrawModifierNode {
private var alpha by mutableFloatStateOf(1f)
override fun onAttach() {
coroutineScope.launch {
while (true) {
animate(1f, 0.3f) { value, _ -> alpha = value; invalidateDraw() }
animate(0.3f, 1f) { value, _ -> alpha = value; invalidateDraw() }
delay(period)
}
}
}
override fun ContentDrawScope.draw() {
drawContent()
drawRect(Color.Black.copy(alpha = 1f - alpha), blendMode = BlendMode.DstIn)
}
}
fun Modifier.pulse(period: Long): Modifier = this then PulseElement(period)The node's built-in coroutineScope is cancelled automatically on onDetach. No leak, no manual DisposableEffect.
CompositionLocal inside a node// WRONG (legacy)
fun Modifier.themedBorder(width: Dp): Modifier = composed {
val tokens = LocalThemeTokens.current
drawBehind { drawRect(tokens.outline, style = Stroke(width.toPx())) }
}
// WRONG because: every CompositionLocal read inside composed { } pins the modifier to a fresh scope per parent recomposition.// RIGHT
private data class ThemedBorderElement(val width: Dp) : ModifierNodeElement<ThemedBorderNode>() {
override fun create(): ThemedBorderNode = ThemedBorderNode(width)
override fun update(node: ThemedBorderNode) { node.width = width }
}
private class ThemedBorderNode(
var width: Dp,
) : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode {
override fun ContentDrawScope.draw() {
val tokens = currentValueOf(LocalThemeTokens)
drawContent()
drawRect(tokens.outline, style = Stroke(width.toPx()))
}
}
fun Modifier.themedBorder(width: Dp): Modifier = this then ThemedBorderElement(width)CompositionLocalConsumerModifierNode exposes currentValueOf(local) from inside any node callback. Reads are tracked by the Draw invalidation list, not by a Composition restart scope, so changing LocalThemeTokens redraws the node without recomposing the parent.
data class (the silent-stale-node bug)// WRONG
private class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
override fun create() = CircleNode(color)
override fun update(node: CircleNode) { node.color = color }
}
// WRONG because: not a data class -> equals() is referential -> every apply looks like a different element -> Compose tears down and recreates the node every time, OR the diff fails and update() is never called, leaving the node with the original color forever.// RIGHT
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
override fun create() = CircleNode(color)
override fun update(node: CircleNode) { node.color = color }
}// WRONG
private class HostingNode(val composer: Composer) : Modifier.Node() { /* ... */ }
// WRONG because: a Modifier.Node outlives any single composition pass; holding a Composer/composition-scoped object leaks it and invokes undefined behavior.// RIGHT — accept primitive/stable parameters; read CompositionLocals via CompositionLocalConsumerModifierNode if you need composition context.
private data class HostingElement(val tag: String) : ModifierNodeElement<HostingNode>() {
override fun create() = HostingNode(tag)
override fun update(node: HostingNode) { node.tag = tag }
}
private class HostingNode(var tag: String) : Modifier.Node() { /* ... */ }DelegatingNode// RIGHT — one public modifier, three small nodes delegated under one element
private data class CardEffectsElement(
val color: Color,
val onClick: () -> Unit,
) : ModifierNodeElement<CardEffectsNode>() {
override fun create() = CardEffectsNode(color, onClick)
override fun update(node: CardEffectsNode) {
node.update(color, onClick)
}
}
private class CardEffectsNode(
color: Color,
onClick: () -> Unit,
) : DelegatingNode() {
private val background = delegate(BackgroundNode(color))
private val click = delegate(ClickNode(onClick))
fun update(color: Color, onClick: () -> Unit) {
background.color = color
click.onClick = onClick
}
}DelegatingNode is the canonical way to assemble multi-behavior modifiers without one node implementing every interface. The delegated children share the host's lifecycle.
Cheat sheet — full override surface and "use when" guidance lives in references/modifier-node-anatomy.md:
DrawModifierNode — implement ContentDrawScope.draw(). Replaces drawBehind/drawWithCache for custom modifiers.LayoutModifierNode — implement MeasureScope.measure(...). Replaces Modifier.layout { }.SemanticsModifierNode — implement SemanticsPropertyReceiver.applySemantics().PointerInputModifierNode — implement onPointerEvent(...) and onCancelPointerInput().CompositionLocalConsumerModifierNode — exposes currentValueOf(local) inside any node callback.LayoutAwareModifierNode — onPlaced(coordinates) / onRemeasured(size).GlobalPositionAwareModifierNode — onGloballyPositioned(coordinates).ObserverModifierNode — wrap state reads with observeReads { ... } and react in onObservedReadsChanged().DelegatingNode — delegate(otherNode) to compose behaviors.TraversableNode — walk parents/children/descendants via the top-level extension functions on DelegatableNode: traverseAncestors(key, block), traverseChildren(key, block), traverseDescendants(key, block). The key parameter selects which traversable nodes participate; the descendants overload's block returns a TraverseDescendantsAction (continue / skip / cancel).ModifierNodeElement.create() // first apply only
↓
Modifier.Node.onAttach() // node joins the tree; coroutineScope becomes valid
↓ // (lives here across many parent recompositions)
ModifierNodeElement.update(node) // each subsequent apply with !equals previous
↓ // mutate node.var fields; auto-invalidates by default
Modifier.Node.onReset() // optional: lazy-layout reuse
↓
Modifier.Node.onDetach() // node leaves the tree; coroutineScope is cancelledModifier.Node for any new custom modifier — Modifier.composed { } is legacy.ModifierNodeElement<T> a data class so the synthesized equals()/hashCode() drive the diff. Plain class silently breaks update().update(node: T) to mutate node state in place. MUST NOT recreate the node from update().onDetach(). coroutineScope is cancelled for you; manual resources are not.Composer, the calling composable, the parent composition, or any composition-scoped object inside a Modifier.Node.CoroutineScope in onAttach — use the built-in coroutineScope property.Modifier.composed { } for new code. (Repo-wide rule from SPEC §8.)DrawModifierNode, LayoutModifierNode, …) over a bare Modifier.Node.DelegatingNode over implementing more than ~3 specialized interfaces on a single node.shouldAutoInvalidate = false and call invalidateDraw() / invalidateMeasurement() / invalidatePlacement() explicitly when fine-grained control matters; otherwise rely on the auto-invalidation triggered by update().grep -R "Modifier.composed" <module>/src returns zero matches in the migrated module.data class Element (search: class .*Element : ModifierNodeElement should be data class).composables.txt) shows the parent composables that consume the migrated modifier are now restartable skippable (no composed-induced non-skip).@TraceRecomposition (skydoves/compose-stability-analyzer) on the parent confirms in release + R8 + real device that the parent is not recomposed by the modifier's internal animation.onAttach (listeners, observers) are released in onDetach — verify with a leak canary pass.references/modifier-node-anatomy.md — full per-interface override surface, lifecycle diagram, DelegatingNode composition recipes, and manual-invalidation guidance.© rosuH, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 1 other file (references) in .agents/skills/migrating-to-modifier-node of rosuH/EasyWatermark.
Open the folder on GitHubat commit 61223db
We found 1 copy of this SKILL.md (exact, near-identical or edited) in other folders, from 1 other GitHub owner. This page covers the copy in rosuH/EasyWatermark, which our catalogue first saw on October 7, 2026.
Migrating To Modifier Node next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Migrating To Modifier Node this skillrosuH/EasyWatermark | 1.9k | 1 repos | ~5k | Automated safety check: Pass | Apache-2.0 | |
| Compose Performance AuditMoustachauve/WLED-Android | 169 | 3 repos | ~1.7k | Automated safety check: Pass | Apache-2.0 | |
| Compose UIMoustachauve/WLED-Android | 169 | 3 repos | ~676 | Automated safety check: Pass | Apache-2.0 | |
| Publish ReleaseAyuilos/Miffan | 182 | — | ~635 | Automated safety check: Pass | AGPL-3.0 | |
| Composewebview Documentationparkwoocheol/compose-webview | 103 | — | ~1.5k | Automated safety check: Pass | MIT | |
| Compose Multiplatform Patternsmonta-app/ocpp-emulator | 179 | 5 repos | ~2k | Automated safety check: Pass | Apache-2.0 |
Moustachauve/WLED-Android
Audit and improve Jetpack Compose runtime performance from code review and architecture.
Moustachauve/WLED-Android
Best practices for building UI with Jetpack Compose, focusing on state hoisting, detailed performance optimizations, and theming.
Ayuilos/Miffan
Publish a GitHub release for this fork, with a bilingual changelog that separates fork-owned changes from changes introduced by upstream merges.
parkwoocheol/compose-webview
Manages MkDocs documentation site and API references for ComposeWebView.
monta-app/ocpp-emulator
Compose Multiplatform and Jetpack Compose patterns for KMP projects — state management, navigation, theming, performance, and platform-specific UI.
dpconde/claude-android-skill
Create production-quality Android applications following Google's official architecture guidance and NowInAndroid best practices.
rosuH/EasyWatermark
A skill your agent uses to push frequently-changing Jetpack Compose state reads (scroll position, animation values, drag offsets) out of the Composition phase and down into Layout or Draw using…
rosuH/EasyWatermark
A skill your agent uses to diagnose Jetpack Compose stability problems by enabling and reading the Compose Compiler Reports (classes.txt, composables.txt, composables.csv, module.json).
rosuH/EasyWatermark
A skill your agent uses to generate and measure Jetpack Compose Baseline Profiles end-to-end with the AGP 8.2+ Baseline Profile Generator module and the Macrobenchmark harness.
rosuH/EasyWatermark
Analyzes Android codebases to implement ML Kit GenAI Prompt API.
rosuH/EasyWatermark
A skill your agent uses to fix unstable Jetpack Compose types once a stability diagnosis has identified them.
rosuH/EasyWatermark
A skill your agent uses to explain why the Compose compiler classified a class or composable parameter as stable, runtime, unknown, or unstable.
Works with
Categories
A skill your agent uses to author new custom Jetpack Compose modifiers and migrate legacy ones from Modifier.composed { } to Modifier.Node + ModifierNodeElement<T. Migrating To Modifier Node is an agent skill from rosuH/EasyWatermark.Node + ModifierNodeElement<T.
Migrating To Modifier Node fits situations like: author new custom Jetpack Compose modifiers and migrate legacy ones from Modifier.composed { } to Modifier.Node + ModifierNodeElement<T; the developer mentions Modifier.composed; custom modifier; modifierNodeElement.
Run `npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a claude-code`. Or copy the skill folder (.agents/skills/migrating-to-modifier-node in rosuH/EasyWatermark) into .claude/skills/migrating-to-modifier-node in your project. Claude Code loads it when a task matches its description.
Run `npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a codex`. Or copy the skill folder (.agents/skills/migrating-to-modifier-node in rosuH/EasyWatermark) into .agents/skills/migrating-to-modifier-node in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add rosuH/EasyWatermark --skill migrating-to-modifier-node -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/migrating-to-modifier-node, .gemini/skills/migrating-to-modifier-node, .github/skills/migrating-to-modifier-node and .opencode/skills/migrating-to-modifier-node in your project.
SKILL.md names no scripts, command-line tools or credentials: Migrating To Modifier Node is instructions for the agent only.
SKILL.md names 4 domains. As links in the text: developer.android.com, medium.com, chrisbanes.me and speakerdeck.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Migrating To Modifier Node is published under the Apache-2.0 licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 5k tokens (SKILL.md is roughly 20k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full. Its references folder adds about 3.5k tokens, read only when the agent opens those files.
Skills that share tags, products or a category with Migrating To Modifier Node: Compose Performance Audit (Moustachauve/WLED-Android, 169 stars), Compose UI (Moustachauve/WLED-Android, 169 stars), Publish Release (Ayuilos/Miffan, 182 stars) and Composewebview Documentation (parkwoocheol/compose-webview, 103 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
rosuH (a GitHub user) maintains it in rosuH/EasyWatermark, which has 1,894 GitHub stars. The repository holds 28 skills in this directory. The repository was last updated on October 6, 2026.
Source: rosuH/EasyWatermark on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.