Skip to content

$changed Bitmask

The Compose compiler injects $changed parameters into every @Composable function. Rebound decodes these bitmasks at runtime to tell you exactly which parameters caused each recomposition.

Each @Composable function receives one or more $changed integer parameters. The bitmask encodes the stability state of each parameter using a fixed bit layout.

$changed bitmask (up to 10 params per mask):
+------+----------+----------+----------+---+
| bit0 | bits 1-3 | bits 4-6 | bits 7-9 |...|
|force | param 0 | param 1 | param 2 | |
+------+----------+----------+----------+---+
  • Bit 0 — the force flag. When set, this recomposition was forced by a parent invalidation.
  • Bits 1-3 — stability state for the first parameter
  • Bits 4-6 — stability state for the second parameter
  • And so on, 3 bits per parameter, up to 10 parameters per $changed integer
BitsValueMeaning
000UNCERTAINThe compiler could not determine whether this parameter changed
001SAMEThe parameter value is the same as the previous composition
010DIFFERENTThe parameter value changed since the previous composition
100STATICThe parameter is a compile-time constant and will never change

For composables with more than 10 parameters, the Compose compiler generates additional masks: $changed1, $changed2, and so on. Rebound collects all masks into a comma-separated string and decodes each one.

The IR transformer extracts parameter names from the function signature and passes them alongside the $changed masks:

ReboundTracker.onComposition(
key = "com.example.ProfileHeader",
budgetClass = LEAF,
changedMask = `$changed`,
paramNames = "avatarUrl,displayName,isOnline",
changedMasks = "\$changed"
)

ChangedMaskDecoder extracts the per-parameter state from the integer bitmask:

// For a composable with 3 params and $changed = 0b0_010_001_010
// Bit 0 (force): 0 -> not forced
// Bits 1-3 (param 0 "avatarUrl"): 010 -> DIFFERENT
// Bits 4-6 (param 1 "displayName"): 001 -> SAME
// Bits 7-9 (param 2 "isOnline"): 010 -> DIFFERENT

This produces the violation output:

params: avatarUrl=DIFFERENT, displayName=SAME, isOnline=DIFFERENT

The Stability tab presents a parameter stability matrix showing each parameter’s state across compositions. This surfaces patterns like “parameter X is always DIFFERENT” that indicate an unstable type or frequently-changing upstream state.

In practice, the Compose compiler reports UNCERTAIN (bits 000) for most parameters in many composables. This happens because:

  • The parameter type is not annotated with @Stable or @Immutable
  • The compiler cannot statically prove stability
  • Strong Skipping Mode may skip the composable at runtime even when the mask says UNCERTAIN, but the mask itself still reads as UNCERTAIN

This is a limitation of the Compose compiler’s static analysis, not a Rebound issue. When you see UNCERTAIN, it means the compiler did not have enough information to determine the parameter’s state at compile time. The runtime may still skip correctly based on equals() checks.

STATIC (bits 100) only appears for parameters that are compile-time constants, such as string literals or hardcoded values. Most real-world parameters are not STATIC.

If parameter state tracking is important for a specific composable, annotate the parameter types with @Stable or @Immutable:

@Stable
data class UserProfile(
val name: String,
val avatarUrl: String,
val isOnline: Boolean
)
@Composable
fun ProfileHeader(user: UserProfile) {
// $changed will now report SAME or DIFFERENT instead of UNCERTAIN
// because UserProfile is @Stable
}

This gives the Compose compiler enough information to produce meaningful bitmask values, which Rebound can then decode and surface.