Jetpack Compose Performance
Android (Google)OfficialCreated by Google LLCDistributed by Android (Google)
- Skill
- Review and audit
- Software development
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured audit workflow for identifying and resolving performance issues in Jetpack Compose applications. It covers state management, layout measurement, lazy container optimization, animation, threading, and custom drawing. The guidelines emphasize phase deferral, proper use of state derivation, and efficient modifier implementation to prevent unnecessary recomposition and rendering overhead.
What it does
- Provides an audit checklist for identifying performance bottlenecks in Compose UI code.
- Explains how to defer state reads to Layout or Draw phases to minimize recomposition.
- Offers patterns for optimizing lazy layouts using keys, content types, and efficient state queries.
- Details how to migrate legacy modifiers to the Modifier.Node API for improved efficiency.
- Defines rules for managing side effects, lifecycle-aware flow collection, and threading.
- Outlines best practices for graphics caching and custom drawing performance.
When to use it
- When analyzing UI performance issues such as dropped frames or layout lag.
- When refactoring complex composables to reduce recomposition overhead.
- When optimizing data models and state handling for large lazy lists.
- When implementing custom modifiers or complex drawing operations.
Advantages
- Reduces redundant recomposition passes through phase deferral techniques.
- Provides clear guidance on avoiding backwards writes and infinite recomposition loops.
- Standardizes the implementation of custom modifiers using modern Modifier.Node APIs.
- Helps maintain UI stability and rendering performance in complex Android applications.
Usage examples
- “How can I optimize a frequently recomposing composable that reads state?”
- The agent will guide you to wrap state reads in lambdas or use Modifier.graphicsLayer to defer reads to the Layout or Draw phases, bypassing unnecessary composition.
- “How should I handle state in a LazyColumn to avoid performance issues?”
- The agent will advise on using primitive keys and content types, hoisting state queries, and avoiding O(N) lookups to ensure efficient scrolling and item reuse.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/android/jetpack-compose-performance/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip jetpack-compose-performance.zip -d .agents/skills
Documentation by Google LLC
Foundational prerequisites
Before micro-optimizing composables, ensure standard production flags are in place:
Release Build: Never profile or benchmark debuggable builds; ART runtime verification severely degrades performance.
R8 Minification: Keep R8 enabled to optimize bytecode and coroutine dispatch.
Baseline Profiles: Generate baseline profiles for critical user journeys (cold start, key scrolling feeds) so the ART VM pre-compiles and preloads classes ahead of time.
Optimization audit workflow
Exhaustive Scan : Scan the target code against all 7 core categories in the following sections. Evaluate every
@Composablefunction, modifier chain, and lazy-layout data model against all checklist rules rather than stopping after the first issue found.Mandatory Reference Read Before Flagging or Editing : Whenever a file matches a rule in Categories 1--7, you MUST open and read the corresponding
references/*.mdfile for the approved fix pattern and full scope guards before reporting a finding or editing code. Never rely on the summary bullets alone to write fixes.
1. State, snapshots, and stability (references/state-and-stability.md)
MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/state-and-stability.md.
Phase Deferral (Lambdas) : Wrap frequently changing state reads in lambdas (such as
Modifier.offsetorModifier.graphicsLayer) so reads occur in Layout or Draw phase rather than Composition.Prohibit Backwards Writes (Verify Read-Before-Write Order) : Never modify state in a later phase or after it has already been read in the same composition pass (
Read -> Write, orComposition Read -> Layout or Draw Write). Prevent backwards writes across phases to avoid first-frame popping, jank, and infinite recomposition loops (Backwards Write). Do not flag a state assignment at the start of a composable (Write -> ReadorComposition Write -> Draw Read) as a backwards write---a backwards write strictly requires a read of that state to precede the write.Avoid Redundant State Wrapping in Composition (
Write -> Read) : Do not wrap synchronous values inremember { mutableStateOf(...) }only to overwrite them immediately at the top of composition before reading them (for example,var path by remember { mutableStateOf(Path()) }; path = viewModel.getPath()). While not a backwards write, this adds unnecessary slot table andMutableStatesnapshot overhead (and risks becoming a backwards write if a read is later added preceding the assignment); use a plain localvalor read the value directly inside the target phase (such asModifier.drawWithCache).Explicit
rememberKeys & Slot Overhead : Always pass all input variables and composable parameters referenced insiderememberas calculation keys. Remember thatrememberstoreskeys + 1objects in the slot table and checks key equality on every recomposition; onlyrememberwhen allocations are heavy or identity must be preserved. Do not pass snapshot state values/properties (e.g.state.value) as keys when wrappingderivedStateOf(passing theStateinstance itself is fine if the instance reference can change).computedStateOfvsderivedStateOf(Compose 1.13+) : On Compose 1.13+, usecomputedStateOffor simple condition checks and filtered state from fast-changing inputs (such asscrollState.value > 0), which provides significantly faster invalidation thanderivedStateOf(on Compose< 1.13, useremember { derivedStateOf { ... } }). ReservederivedStateOffor heavy calculations that benefit from multi-read caching, or trivial calculations read frequently enough per composition (roughly10 / numberOfStatesReadPerCalculationtimes without invalidating between reads) to make up for the invalidation cost.Synchronous State Derivation : Audit for asynchronous
produceState+snapshotFlowblocks observing Compose states (such as scroll positions or collection sizes); replace them with synchronousremember { derivedStateOf { ... } }(orcomputedStateOfon Compose 1.13+).Trivial Primitives : Prefer plain custom Kotlin getters (
get()) overderivedStateOforcomputedStateOffor simple boolean expressions and property lookups.Collection State Tracking : Back whole-list updates with standard
mutableListOf()andmutableStateOf(list, neverEqualPolicy())with load-bearing assignments instead ofmutableStateListOf().Snapshot Collections : Use
SnapshotStateList.toList()for repeated queries or iterations to avoid per-element read observation overhead.Repeated State Reads : Extract repeated Compose
Stateproperty reads into a local variable at the top of the block.State in Setters : Avoid reading Compose
Stateinside custom property setters; use a raw backing field when triggering change side-effects and batch multi-property updates.Stability & Skipping Trade-Offs (Skipping Isn't Free) : Do not fixate on stability or treat
@Immutable,@Stable, orImmutableListas default solutions. Skipping a composable is not free: each parameter is stored in the slot table as if remembered, and all inputs must be compared on recomposition. Strong Skipping Mode already skips composables with unstable parameters (including standardkotlin.collections.List) when instance equality (===) matches. Annotating models or usingImmutableListforces structural equality (.equals()), which can be significantly more expensive than recomposition itself for large collections or complex classes.Never Add
@Stable,@Immutable, orImmutableListto Collection Models : When writing or optimizing code, never add@Immutableor@Stableto models containing collections (List,Set,Map), and never recommend replacingkotlin.collections.List<T>withImmutableList<T>(or addingImmutableListtostability_config.txt). CallingO(N).equals()on collections on every recomposition pass is more expensive thanO(1)instance equality (===) under Strong Skipping Mode.Scope Guard for Removing Existing
@Stable/@ImmutableAnnotations (Large Lazy Datasets Only) :When to Remove : Only recommend removing existing
@Stableor@Immutableannotations from data models when both conditions are met: (1) the model is used within a lazy layout (LazyColumn,LazyRow,LazyGrid,Pager,ScalingLazyColumn) as the input feed or item model, and (2) it contains a large dataset or deep nested collection hierarchy (e.g., feed streams, large lists of tags/items, or multi-level nested lists likePostwithList<Paragraph>andList<Markup>), where recursiveO(N).equals()checks degrade scrolling and recomposition performance.When to Leave Untouched : For all other existing
@Stable,@Immutable, orImmutableListusages in a codebase---such as non-lazy UI models, standalone components, or models with small/bounded collections (e.g., a card with a few badges, thumbnails, or metadata rows) where dataset size is small or unknown---leave the existing annotations untouched and do not flag them . Never blindly grep and flag all@Stable/@Immutableclasses across a codebase without verifying they feed a lazy layout with a large dataset.
When to Add
@Stable/@ImmutableUnder Strong Skipping Mode : Flatdata classes withvalprimitive/Stringproperties in Compose modules are already inferred as stable by the compiler and use.equals()automatically---do not add redundant@Immutableannotations to them. Only add@Stableor@Immutablewhen the compiler cannot infer stability (e.g. classes from non-Compose modules or interfaces) and a data source (such as Room or MVI reducers) allocates new object instances for unchanged items on every emission, provided the class is small, flat, has no collections (List,Set,Map), and its.equals()check is trivialO(1).Make Composables Cheaper Instead of Forcing Skipping: Do not overindex on recomposition counts. Skipping one composable during an active recomposition pass saves little time once composition is scheduled (scheduling and starting the recomposition pass is the bulk of the cost). Focus on making composables cheap to execute, deferring state reads to Layout/Draw, and verifying with benchmarks rather than assuming skipping improves performance.
2. Layouts and measurement optimization (references/layouts.md)
MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/layouts.md.
Coordinates & Placement : Avoid
onGloballyPositioned; useModifier.onVisibilityChangedfor viewport tracking,Modifier.onPlacedorModifier.onLayoutRectChangedfor local bounds, and queryrequireLayoutCoordinates()on demand insideModifier.Nodeevent handlers.Anchor Floating Content via Custom
Layout: When attaching floating labels or tooltips to a component, use a customLayoutmeasuring and placing both children relative to each other. Never trackonGloballyPositionedcoordinates inMutableStateto position floating content withoffset, which causes a 1-frame lag and schedules a second composition pass.Phase Deferral for Offsets : Wrap dynamic offsets in lambdas (
Modifier.offset { IntOffset(...) }) to skip Composition straight to Layout.Tree Depth & Decorative Wrappers : Avoid redundant wrapper
Boxes and decorativeCanvasnodes; attach modifiers directly or useModifier.drawBehind.Subcomposition Overhead : Avoid
BoxWithConstraints/SubcomposeLayoutinside hot paths and lazy items; prefer standard modifiers,Modifier.layout, orLayoutModifierNode.Intrinsic Measurements: Use intrinsic measurements judiciously and avoid deeply nested intrinsic chains.
3. Lazy layouts optimization (references/lazy-layouts.md)
MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/lazy-layouts.md.
Lazy Layout Scope Guard : Directives for
keyandcontentTypeapply strictly to Lazy containers (LazyColumn,LazyRow,LazyGrid). Never flag standard non-lazy containers (Column,Row,Box) iterating over collections.Mandatory
contentTypeand Primitivekey: Always specify both primitivekeys (Int,Long,String) andcontentTypes for all items in lazy layouts (LazyColumn,LazyRow,LazyGrid). When usingitemsoritemsIndexed, never omitcontentType:contentType = { _, item -> item.type }. OmittingcontentTypeprevents Compose from reusing composition slots during scrolling.In-Item Index Lookups : Replace
list.indexOf(item)withitemsIndexedto eliminateO(N²)lookups during layout passes.Observable Selection & State Queries : Convert selection tracking from
mutableStateListOfto an immutableSetinMutableState(mutableStateOf(emptySet<String>())),mutableStateSetOf(), or Map forO(1)lookups. Pass lambda providers (isSelected: () -> Boolean) to leaf composables to isolate recomposition from parentitemsscopes.Propagate CompositionLocals in Lazy Items : Avoid querying
CompositionLocals (LocalContext.current,LocalDensity.current,LocalSharedTransitionScope.current, scopes, etc.) inside individual lazy item composables; resolve them once in the parent container or screen and propagate the resolved values down as parameters.Defer Shared Transitions in Lazy Items : Put
Modifier.sharedBoundsorModifier.sharedElementon lazy items behind visibility indicators (such asModifier.onVisibilityChangedor viewport visibility checks) so offscreen or pre-fetched items do not instantiate transition scopes.Item Count Derivations : Read collection
.sizedirectly instead of wrapping list counts inderivedStateOf.Small & Bounded Lists : Use standard
RoworColumnwithfororforEachloops for small or bounded collections (e.g. 1 to 10 items) to eliminate lazy virtualization, measuring, and pre-fetching overhead.Hoist Shared Flows : Collect shared
Flows once in the parent container preceding lazy layouts; never callcollectAsStateWithLifecycle()per visible item.Avoid Side Effects in Pre-Fetched Items : Never trigger analytics in
LaunchedEffect(Unit)inside lazy items; useModifier.onVisibilityChangedwith dwell thresholds instead.
4. Animation and shared transitions (references/animation.md)
MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/animation.md.
Phase Deferral for Animated Properties : Wrap fast-changing animated state reads (alpha, scale, rotation, color) in
Modifier.graphicsLayer { ... }to bypass Composition and evaluate directly in the Draw phase.Phase Deferral for Offsets & Size : Wrap animated offsets in
Modifier.offset { IntOffset(...) }and animate constraints usingModifier.layout { ... }orModifier.animateBoundsto bypass recomposition during size transitions.Defer Shared Transitions in Fast-Scrolling Lists : Attach
Modifier.sharedBoundsorModifier.sharedElementonly when visible usingModifier.onVisibilityChangedto avoid modifier and observation instantiation costs on discarded pre-fetched items.Lambda Providers : Pass lambda providers (
() -> Float) instead of raw animated values to child composables to keep state reads deferred to Draw or Layout.Persistent
Animatablein Nodes : StoreAnimatableas a persistent member property onModifier.Nodeand launch inonAttach()usingcoroutineScope.launch.
5. Effects, lifecycle, and threading (effects-and-threading.md)
MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/effects-and-threading.md.
Prohibit Direct Side-Effects in Body : Never perform side-effects directly in the Composable body during composition; wrap them in
SideEffect,LaunchedEffect, or user event callbacks.Prohibit Non-Suspending
LaunchedEffect& EmptyDisposableEffect: Never useLaunchedEffectfor synchronous operations (e.g. logging views or updating objects). Never useDisposableEffectwith an emptyonDispose {}block as a workaround. UseSideEffectinstead (in Compose 1.12+,SideEffect(keys) { ... }supports keys directly; on Compose< 1.12,SideEffecttakes no arguments, so check the project's Compose version and track key changes with aremembered holder insideSideEffect { ... }).Cache Allocations in Body : Wrap heavy object allocations (such as date formatters or complex paths) and expensive computations in
remember.Combine Related
LaunchedEffects : Combine multipleLaunchedEffect(Unit)calls into a singleLaunchedEffectwith nestedlaunch { ... }blocks to reduce effect node allocations and coroutine launcher overhead.Effect Ordering & Teardown Costs : Understand effect execution order when composition changes are applied (
DisposableEffectruns first, thenSideEffect, whileLaunchedEffectlaunches a coroutine on the composition'sCoroutineContextthat is dispatched asynchronously and is not guaranteed to wait until after Draw). Note that canceling aLaunchedEffectincurs coroutine cancellation and teardown overhead; avoid cyclingLaunchedEffectkeys frequently during animations or scrolling.Main Thread Offloading : Offload blocking I/O, heavy file parsing, and database operations to
withContext(Dispatchers.IO)usingproduceState.No Item-Level Impression Effects in Lazy Layouts : Never use
LaunchedEffectorDisposableEffectinside lazy items for impression logging. Lazy items are pre-composed before entering the viewport, causing premature or false impressions. UseModifier.onVisibilityChangedwith dwell thresholds instead.Lifecycle-Aware Flow Collection : Use
collectAsStateWithLifecycle()for UI Flow collections that actively produce background work on Android, andcollectAsState()for lightweight in-memory StateFlows or non-Android scopes.Hoist Receivers & Listeners : Register system listeners and
BroadcastReceivers once at the screen level usingDisposableEffect, not inside lazy list items.Guard Optional Effects : Guard effects with
if (callback != null)before launching to prevent allocating empty coroutines and effect nodes.Prevent Stale Captures : Use
rememberUpdatedStatewhen long-running or looping coroutines inLaunchedEffectreference callbacks or parameters.Replace Legacy Timers : Replace
Handler.postDelayedorTimerTaskwithLaunchedEffectanddelay().
6. Graphics and custom drawing (references/graphics-performance.md)
MANDATORY : Before flagging or fixing any issue in this category, you MUST read references/graphics-performance.md.
Cache Drawing Allocations : Replace decorative
Canvasnodes withModifier.drawWithCacheto cachePath,Paint, andBrushallocations.Prefer AsyncImage instead of Image : Use Coil's
AsyncImagefor loading images from the internet andpainterResourcefor drawables.Imageis not optimized for caching and loading images and may cause performance issues.Scope Fast-Changing Uniforms :
remembertheRuntimeShaderandShaderBrushin composition, update size uniforms indrawWithCache, and per-frame uniforms inonDrawBehind.Graphics Path Object Reuse : Prefer
Path.rewind()overreset()in hot draw loops to retain allocated buffers without reallocation.Value Classes & Primitive Collections : Avoid generic tuples (
Pair,Triple) and generic sets (HashSet<Long>); useandroidx.collection.MutableLongSetandandroidx.compose.ui.util.packInts.
7. Custom modifiers: Modifier.Node (Custom modifiers guide)
Modifier.Node Migration : Migrate legacy
Modifier.composedtoModifierNodeElement+Modifier.Node(orDelegatingNodewhen delegating to sub-nodes such asSuspendingPointerInputModifierNode) with capability interfaces (DrawModifierNode,LayoutModifierNode,SemanticsModifierNode,CompositionLocalConsumerModifierNodeusingcurrentValueOf(...)) to eliminate per-node subcomposition overhead and enable node reuse (see Create custom modifiers with Modifier.Node):// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color) // ModifierNodeElement private data class CircleElement( val color: Color ) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } } // Modifier.Node private class CircleNode( var color: Color ) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }<br />
On-Demand Coordinates : Query
requireLayoutCoordinates()on demand inside pointer or draw handlers inModifier.Noderather than implementingGlobalPositionAwareModifierNode.onGloballyPositioned.
IMPORTANT : For more examples on how to migrate common modifiers to Modifier.Node, you must read Create custom modifiers with Modifier.Node.
Files
- ATTRIBUTION.mdAdded by AutoGineer1010 B
- LICENSELicence file added by AutoGineer11.1 KiB
- SKILL.md20.5 KiB
- references/android/develop/ui/compose/custom-modifiers.md26.7 KiB
- references/android/develop/ui/compose/performance/backwards-write.md12.0 KiB
- references/android/develop/ui/compose/performance/stability/strongskipping.md6.6 KiB
- references/animation.md8.0 KiB
- references/effects-and-threading.md17.3 KiB
- references/graphics-performance.md9.4 KiB
- references/layouts.md8.8 KiB
- references/lazy-layouts.md12.5 KiB
- references/state-and-stability.md27.5 KiB
references/android/develop/ui/compose/custom-modifiers.md
Compose provides many modifiers for common behaviors right out of the box, but you can also create your own custom modifiers.
Modifiers have multiple parts:
A modifier factory
This is an extension function on
Modifier, which provides an idiomatic API for your modifier and allows modifiers to be chained together. The modifier factory produces the modifier elements used by Compose to modify your UI.
A modifier element
This is where you can implement the behavior of your modifier.
There are multiple ways to implement a custom modifier depending on the
functionality needed. Often, the simplest way to implement a custom modifier is
to implement a custom modifier factory that combines other already defined
modifier factories. If you need more custom behavior, implement the modifier
element using the Modifier.Node APIs, which are lower level but provide more
flexibility.
Chain existing modifiers together
It is often possible to create custom modifiers by using existing modifiers. For
example, Modifier.clip() is implemented using the graphicsLayer
modifier. This strategy uses existing modifier elements, and you provide your
own custom modifier factory.
Before implementing your own custom modifier, see if you can use the same strategy.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)<br />
Or, if you find you are repeating the same group of modifiers often, you can wrap them into your own modifier:
fun Modifier.myBackground(color: Color) = padding(16.dp)
.clip(RoundedCornerShape(8.dp))
.background(color)<br />
Create a custom modifier using a composable modifier factory
You can also create a custom modifier using a composable function to pass values to an existing modifier. This is known as a composable modifier factory.
[!NOTE] Note:
composed {}is no longer recommended due to performance concerns. See Migrate fromModifier.composedtoModifier.Nodefor details on how to migrate away from this modifier.
Using a composable modifier factory to create a modifier also lets you use
higher level compose APIs, such as animate*AsState and other Compose
state backed animation APIs. For example, the following snippet shows a
modifier that animates an alpha change when enabled/disabled:
@Composable
fun Modifier.fade(enable: Boolean): Modifier {
val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
return this then Modifier.graphicsLayer { this.alpha = alpha }
}<br />
[!WARNING] Warning: When creating custom modifiers, don't break the modifier chain. You must always reference
thisor else any modifiers previously added will be dropped. You can usethis then Modifieras in the preceding example or implicitly usingreturn graphicsLayer { this.alpha = alpha }.
If your custom modifier is a convenience method to provide default values from a
CompositionLocal, the easiest way to implement this is to use a composable
modifier factory:
@Composable
fun Modifier.fadedBackground(): Modifier {
val color = LocalContentColor.current
return this then Modifier.background(color.copy(alpha = 0.5f))
}<br />
This approach has some caveats, which are detailed in the following sections.
CompositionLocal values are resolved at the call site of the modifier factory
When creating a custom modifier using a composable modifier factory, composition locals take the value from the composition tree where they are created, not used. This can lead to unexpected results. For example, consider the composition local modifier example mentioned previously, implemented slightly differently using a composable function:
@Composable
fun Modifier.myBackground(): Modifier {
val color = LocalContentColor.current
return this then Modifier.background(color.copy(alpha = 0.5f))
}
@Composable
fun MyScreen() {
CompositionLocalProvider(LocalContentColor provides Color.Green) {
// Background modifier created with green background
val backgroundModifier = Modifier.myBackground()
// LocalContentColor updated to red
CompositionLocalProvider(LocalContentColor provides Color.Red) {
// Box will have green background, not red as expected.
Box(modifier = backgroundModifier)
}
}
}<br />
If this is not how you expect your modifier to work, use a custom
Modifier.Node instead, as composition locals will be
correctly resolved at the usage site and can be safely hoisted.
Composable function modifiers are never skipped
Composable factory modifiers are never skipped because composable functions that have return values cannot be skipped. This means your modifier function will be called on every recomposition, which may be expensive if it recomposes frequently.
Composable function modifiers must be called within a composable function
Like all composable functions, a composable factory modifier must be called from within composition. This limits where a modifier can be hoisted to, as it can never be hoisted out of composition. In comparison, non-composable modifier factories can be hoisted out of composable functions to allow easier reuse and improve performance:
val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations
@Composable
fun Modifier.composableModifier(): Modifier {
val color = LocalContentColor.current.copy(alpha = 0.5f)
return this then Modifier.background(color)
}
@Composable
fun MyComposable() {
val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher
}<br />
Implement custom modifier behavior using Modifier.Node
Modifier.Node is a lower level API for creating modifiers in Compose. It
is the same API that Compose implements its own modifiers in and is the most
performant way to create custom modifiers.
[!NOTE] Note: There is another API for creating custom modifiers,
composed {}. This API is no longer recommended due to the performance issues it created.Modifier.Nodewas designed from the ground up to be far more performant than composed modifiers. For more details on the problems with composed modifiers, see the Android Dev Summit talk Compose Modifiers Deep Dive.
Implement a custom modifier using Modifier.Node
There are three parts to implementing a custom modifier using Modifier.Node:
A
Modifier.Nodeimplementation that holds the logic and state of your modifier.A
ModifierNodeElementthat creates and updates modifier node instances.An optional modifier factory, as detailed previously.
ModifierNodeElement classes are stateless and new instances are allocated each
recomposition, whereas Modifier.Node classes can be stateful and will survive
across multiple recompositions, and can even be reused.
The following section describes each part and shows an example of building a custom modifier to draw a circle.
Modifier.Node
The Modifier.Node implementation (in this example, CircleNode) implements
the functionality of your custom modifier.
// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
override fun ContentDrawScope.draw() {
drawCircle(color)
}
}<br />
In this example, it draws the circle with the color passed in to the modifier function.
A node implements Modifier.Node as well as zero or more node types. There are
different node types based on the functionality your modifier requires. The
preceding example needs to be able to draw, so it implements DrawModifierNode,
which lets it override the draw method.
The available types are as follows:
|---|---|---|
| Node | Usage | Sample Link |
| LayoutModifierNode | A Modifier.Node that changes how its wrapped content is measured and laid out. | Sample |
| DrawModifierNode | A Modifier.Node that draws into the space of the layout. | Sample |
| CompositionLocalConsumerModifierNode | Implementing this interface lets your Modifier.Node read composition locals. | Sample |
| SemanticsModifierNode | A Modifier.Node that adds semantics key/value for use in testing, accessibility, and similar use cases. | Sample |
| PointerInputModifierNode | A Modifier.Node that receives PointerInputChanges. | Sample |
| ParentDataModifierNode | A Modifier.Node that provides data to the parent layout. | Sample |
| LayoutAwareModifierNode | A Modifier.Node which receives onMeasured and onPlaced callbacks. | Sample |
| GlobalPositionAwareModifierNode | A Modifier.Node which receives an onGloballyPositioned callback with the final LayoutCoordinates of the layout when the global position of the content may have changed. | Sample |
| ObserverModifierNode | Modifier.Nodes that implement ObserverNode can provide their own implementation of onObservedReadsChanged that will be called in response to changes to snapshot objects read within an observeReads block. | Sample |
| DelegatingNode | A Modifier.Node which is able to delegate work to other Modifier.Node instances. This can be useful to compose multiple node implementations into one. | Sample |
| TraversableNode | Allows Modifier.Node classes to traverse up/down the node tree for classes of the same type or for a particular key. | Sample |
Nodes are automatically invalidated when update is called on their corresponding
element. Because our example is a DrawModifierNode, any time update is called
on the element, the node triggers a redraw and its color correctly updates. It
is possible to opt out of auto-invalidation, as detailed in the
Opt out of node auto-invalidation section.
ModifierNodeElement
A ModifierNodeElement is an immutable class that holds the data to create or
update your custom modifier:
// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
override fun create() = CircleNode(color)
override fun update(node: CircleNode) {
node.color = color
}
}<br />
ModifierNodeElement implementations need to override the following methods:
create: This is the function that instantiates your modifier node. This gets called to create the node when your modifier is first applied. Usually, this amounts to constructing the node and configuring it with the parameters that were passed in to the modifier factory.update: This function is called whenever this modifier is provided in the same spot this node already exists, but a property has changed. This is determined by theequalsmethod of the class. The modifier node that was previously created is sent as a parameter to theupdatecall. At this point, you should update the nodes' properties to correspond with the updated parameters. The ability for nodes to be reused this way is key to the performance gains thatModifier.Nodebrings; therefore, you must update the existing node rather than creating a new one in theupdatemethod. In our circle example, the color of the node is updated.
Additionally, ModifierNodeElement implementations also need to implement
equals and hashCode. update will only get called if an equals comparison
with the previous element returns false.
[!WARNING] Warning: Your
ModifierNodeElementmust implementequalsandhashCodecorrectly and not rely on instance equality. Without this, your modifier node will be updated unnecessarily and perform poorly. Use a data class to achieve this automatically.
The preceding example uses a data class to achieve this. These methods are used
to check if a node needs updating or not. If your element has properties that
don't contribute to whether a node needs to be updated, or you want to avoid
data classes for binary compatibility reasons, then you can manually implement
equals and hashCode, for example, the
padding modifier element.
Modifier factory
This is the public API surface of your modifier. Most implementations create the modifier element and add it to the modifier chain:
// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)<br />
Complete example
These three parts come together to create the custom modifier to draw a circle
using the Modifier.Node APIs:
// Modifier factory
fun Modifier.circle(color: Color) = this then CircleElement(color)
// ModifierNodeElement
private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() {
override fun create() = CircleNode(color)
override fun update(node: CircleNode) {
node.color = color
}
}
// Modifier.Node
private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() {
override fun ContentDrawScope.draw() {
drawCircle(color)
}
}<br />
Common situations using Modifier.Node
When creating custom modifiers with Modifier.Node, here are some common
situations you might encounter.
Zero parameters
If your modifier has no parameters, then it never needs to update and, furthermore, doesn't need to be a data class. The following is a sample implementation of a modifier that applies a fixed amount of padding to a composable:
fun Modifier.fixedPadding() = this then FixedPaddingElement
data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() {
override fun create() = FixedPaddingNode()
override fun update(node: FixedPaddingNode) {}
}
class FixedPaddingNode : LayoutModifierNode, Modifier.Node() {
private val PADDING = 16.dp
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val paddingPx = PADDING.roundToPx()
val horizontal = paddingPx * 2
val vertical = paddingPx * 2
val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
val width = constraints.constrainWidth(placeable.width + horizontal)
val height = constraints.constrainHeight(placeable.height + vertical)
return layout(width, height) {
placeable.place(paddingPx, paddingPx)
}
}
}<br />
Reference composition locals
Modifier.Node modifiers don't automatically observe changes to Compose state
objects, like CompositionLocal. The advantage Modifier.Node modifiers have
over modifiers that are just created with a composable factory is that they can
read the value of the composition local from where the modifier is used in your
UI tree, not where the modifier is allocated, using currentValueOf.
However, modifier node instances don't automatically observe state changes. To automatically react to a composition local changing, you can read its current value inside a scope:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScope&IntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
This example observes the value of LocalContentColor to draw a background
based on its color. As ContentDrawScope does observe snapshot changes, this
automatically redraws when the value of LocalContentColor changes:
class BackgroundColorConsumerNode :
Modifier.Node(),
DrawModifierNode,
CompositionLocalConsumerModifierNode {
override fun ContentDrawScope.draw() {
val currentColor = currentValueOf(LocalContentColor)
drawRect(color = currentColor)
drawContent()
}
}<br />
To react to state changes outside of a scope and automatically update your
modifier, use an ObserverModifierNode.
For example, Modifier.scrollable uses this technique to
observe changes in LocalDensity. A simplified example is shown in the
following example:
class ScrollableNode :
Modifier.Node(),
ObserverModifierNode,
CompositionLocalConsumerModifierNode {
// Place holder fling behavior, we'll initialize it when the density is available.
val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity))
override fun onAttach() {
updateDefaultFlingBehavior()
observeReads { currentValueOf(LocalDensity) } // monitor change in Density
}
override fun onObservedReadsChanged() {
// if density changes, update the default fling behavior.
updateDefaultFlingBehavior()
}
private fun updateDefaultFlingBehavior() {
val density = currentValueOf(LocalDensity)
defaultFlingBehavior.flingDecay = splineBasedDecay(density)
}
}<br />
Animate a modifier
Modifier.Node implementations have access to a coroutineScope. This allows
for use of the Compose Animatable APIs. For example, this snippet modifies
the CircleNode shown previously to fade in and out repeatedly:
[!NOTE] Note:
Modifier.Nodecan be reused in a composable. You can set the new state either by setting it inonAttach, or by resetting inonReset.
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode {
private lateinit var alpha: Animatable<Float, AnimationVector1D>
override fun ContentDrawScope.draw() {
drawCircle(color = color, alpha = alpha.value)
drawContent()
}
override fun onAttach() {
alpha = Animatable(1f)
coroutineScope.launch {
alpha.animateTo(
0f,
infiniteRepeatable(tween(1000), RepeatMode.Reverse)
) {
}
}
}
}<br />
Share state between modifiers using delegation
Modifier.Node modifiers can delegate to other nodes. There are many use cases
for this, such as extracting common implementations across different modifiers,
but it can also be used to share common state across modifiers.
For example, a basic implementation of a clickable modifier node that shares interaction data:
class ClickableNode : DelegatingNode() {
val interactionData = InteractionData()
val focusableNode = delegate(
FocusableNode(interactionData)
)
val indicationNode = delegate(
IndicationNode(interactionData)
)
}<br />
Opt out of node auto-invalidation
Modifier.Node nodes automatically invalidate when their corresponding
ModifierNodeElement calls update. For complex modifiers, you might want to opt
out of this behavior to gain more fine-grained control over when your modifier
invalidates phases.
This is particularly useful if your custom modifier modifies both layout and
draw. Opting out of auto-invalidation lets you just invalidate draw when only
draw-related properties, such as color, change. This avoids invalidating
layout and can improve your modifier's performance.
A hypothetical example of this is shown in the following example with a modifier
that has a color, size, and onClick lambda as properties. This modifier
invalidates only what is required, skipping any unneeded invalidation:
class SampleInvalidatingNode(
var color: Color,
var size: IntSize,
var onClick: () -> Unit
) : DelegatingNode(), LayoutModifierNode, DrawModifierNode {
override val shouldAutoInvalidate: Boolean
get() = false
private val clickableNode = delegate(
ClickablePointerInputNode(onClick)
)
fun update(color: Color, size: IntSize, onClick: () -> Unit) {
if (this.color != color) {
this.color = color
// Only invalidate draw when color changes
invalidateDraw()
}
if (this.size != size) {
this.size = size
// Only invalidate layout when size changes
invalidateMeasurement()
}
// If only onClick changes, we don't need to invalidate anything
clickableNode.update(onClick)
}
override fun ContentDrawScope.draw() {
drawRect(color)
}
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val size = constraints.constrain(size)
val placeable = measurable.measure(constraints)
return layout(size.width, size.height) {
placeable.place(0, 0)
}
}
}<br />
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Google LLC
- Publisher
- Android (Google) (Official)
- Distributed by
- Android (Google)
- Source
- android/skills · jetpack-compose/jetpack-compose-performance · commit bf68e2d
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE.txt)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Google LLC or Android (Google).
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download jetpack-compose-performance.zip