You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
D/Recomposition: └─ user: User stable (User@abc123)
320
+
D/Recomposition: └─ [param] user: User stable (User@abc123)
321
321
```
322
322
323
323
This is also very useful if you want to set a custom logger for `ComposeStabilityAnalyzer`, to distinguish which composable function should be examined like the example below:
@@ -415,7 +415,7 @@ Let's understand what each log tells you:
415
415
416
416
```
417
417
D/Recomposition: [Recomposition #1] UserProfile
418
-
D/Recomposition: └─ user: User stable (User@abc123)
418
+
D/Recomposition: └─ [param] user: User stable (User@abc123)
419
419
```
420
420
421
421
**What this means:**
@@ -431,7 +431,7 @@ This log confirms the composable is working correctly. The parameter is stable a
431
431
432
432
```
433
433
D/Recomposition: [Recomposition #2] UserProfile
434
-
D/Recomposition: └─ user: User changed (User@abc123 → User@def456)
434
+
D/Recomposition: └─ [param] user: User changed (User@abc123 → User@def456)
435
435
```
436
436
437
437
**What this means:**
@@ -445,17 +445,17 @@ This is normal behavior. The parameter changed, so the composable recomposed to
D/Recomposition: └─ [param] onClick: () -> Unit stable (Function@xyz)
542
542
543
543
(No more excessive recompositions!)
544
544
```
545
545
546
+
### Internal State Tracking
547
+
548
+
By default, `@TraceRecomposition` only tracks **parameter changes**. When a composable recomposes due to internal state changes (`mutableStateOf`, `derivedStateOf`, etc.), the standard logs show nothing because the parameters haven't changed.
549
+
550
+
Setting `traceStates = true` enables **internal state tracking**:
551
+
552
+
```kotlin
553
+
@TraceRecomposition(traceStates =true)
554
+
@Composable
555
+
funCounterScreen(title:String) {
556
+
var counter by remember { mutableIntStateOf(0) }
557
+
val doubled by remember { derivedStateOf { counter *2 } }
D/Recomposition: ├─ [state] counter: Int changed (0 → 1)
574
+
D/Recomposition: └─ State changes: [counter]
575
+
```
576
+
577
+
The `[param]` prefix identifies parameter entries, while `[state]` identifies internal state changes. Only states that actually changed are logged. This helps answer the question: "Why is this composable recomposing when parameters haven't changed?"
578
+
579
+
> **Note**: State tracking detects delegated state properties (`var x by remember { mutableStateOf(...) }`). Non-delegated patterns (`val state = mutableStateOf(...)`) are not tracked in the current version.
580
+
546
581
### Best Practices
547
582
548
583
**1. Don't track everything**: Be selective about which composables you track. Focus on:
Copy file name to clipboardExpand all lines: docs/gradle-plugin/trace-recomposition.md
+54-13Lines changed: 54 additions & 13 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -21,9 +21,9 @@ When this composable recomposes, detailed logs appear in Logcat showing the reco
21
21
22
22
```
23
23
D/Recomposition: [Recomposition #1] UserProfile
24
-
D/Recomposition: └─ user: User stable (User@abc123)
24
+
D/Recomposition: └─ [param] user: User stable (User@abc123)
25
25
D/Recomposition: [Recomposition #2] UserProfile
26
-
D/Recomposition: └─ user: User changed (User@abc123 → User@def456)
26
+
D/Recomposition: └─ [param] user: User changed (User@abc123 → User@def456)
27
27
```
28
28
29
29
The first log entry shows the initial composition: the `user` parameter is stable and the log includes its identity hash. The second entry shows a recomposition triggered by the `user` parameter changing to a different instance, with both the old and new identity hashes displayed.
@@ -46,7 +46,7 @@ Logs now include the tag, making it easy to filter in Logcat:
D/Recomposition: └─ user: User stable (User@abc123)
49
+
D/Recomposition: └─ [param] user: User stable (User@abc123)
50
50
```
51
51
52
52
Tags are also valuable when using a [custom logger](custom-logger.md), since you can route events differently based on their tag, for example sending only critical flow recompositions (checkout, authentication) to your analytics platform while logging everything else to Logcat during development.
@@ -69,6 +69,47 @@ fun FrequentlyRecomposingScreen() {
69
69
70
70
A threshold of `3` works well for most composables. For composables in scrolling lists or frequently updating screens where some recomposition is expected, a higher threshold (e.g., `10` or `20`) helps focus on truly excessive recomposition.
71
71
72
+
### The `traceStates` Parameter
73
+
74
+
By default, `@TraceRecomposition` only tracks **parameter changes**. When a composable recomposes due to internal state changes (`mutableStateOf`, `derivedStateOf`, etc.), the standard logs show nothing because the parameters haven't changed.
75
+
76
+
Setting `traceStates = true` enables **internal state tracking**. The compiler plugin detects state variable declarations in your composable and injects tracking code that logs which states changed between recompositions.
77
+
78
+
```kotlin
79
+
@TraceRecomposition(traceStates =true)
80
+
@Composable
81
+
funCounterScreen(title:String) {
82
+
var counter by remember { mutableIntStateOf(0) }
83
+
val doubled by remember { derivedStateOf { counter *2 } }
84
+
85
+
Column {
86
+
Text("$title: $counter (doubled: $doubled)")
87
+
Button(onClick = { counter++ }) {
88
+
Text("Increment")
89
+
}
90
+
}
91
+
}
92
+
```
93
+
94
+
After clicking the button, the log now shows both parameter and state information:
D/Recomposition: ├─ [state] counter: Int changed (0 → 1)
100
+
D/Recomposition: └─ State changes: [counter]
101
+
```
102
+
103
+
The `[param]` prefix identifies parameter tracking entries, while `[state]` identifies internal state changes. Only states that actually changed are logged, reducing noise. The `State changes` summary at the end lists all changed state variable names for quick reference.
104
+
105
+
!!! note "Supported state patterns"
106
+
107
+
State tracking detects delegated state properties: `var x by remember { mutableStateOf(...) }`, `mutableIntStateOf`, `mutableLongStateOf`, `mutableFloatStateOf`, `mutableDoubleStateOf`, and `derivedStateOf`. Non-delegated patterns (`val state = mutableStateOf(...)`) are not tracked in the current version. Use the `by` delegation syntax for full tracking support.
108
+
109
+
!!! tip "When to use traceStates"
110
+
111
+
Use `traceStates = true` when a composable is recomposing but the parameter logs show no changes. This typically means an internal state or `CompositionLocal` is causing the recomposition, and state tracking will reveal which one.
112
+
72
113
## Reading the Logs
73
114
74
115
Understanding the log output is key to diagnosing recomposition issues. Each log entry contains several pieces of information that, together, tell you exactly what happened and why.
@@ -77,7 +118,7 @@ Understanding the log output is key to diagnosing recomposition issues. Each log
77
118
78
119
```
79
120
D/Recomposition: [Recomposition #1] UserProfile
80
-
D/Recomposition: └─ user: User stable (User@abc123)
121
+
D/Recomposition: └─ [param] user: User stable (User@abc123)
81
122
```
82
123
83
124
The `[Recomposition #1]` counter tells you this is the first time this composable instance is recomposing. `user: User` identifies the parameter by name and type. The `stable` label means the Compose compiler considers this parameter stable, so it won't cause unnecessary recompositions. The identity hash `(User@abc123)` lets you track whether the same instance is being passed across recompositions.
@@ -88,7 +129,7 @@ This log confirms the composable is working correctly. A stable parameter on the
88
129
89
130
```
90
131
D/Recomposition: [Recomposition #2] UserProfile
91
-
D/Recomposition: └─ user: User changed (User@abc123 → User@def456)
132
+
D/Recomposition: └─ [param] user: User changed (User@abc123 → User@def456)
92
133
```
93
134
94
135
The `changed` label is the most important signal. It tells you this parameter's value is different from the last composition, which is the **reason** this composable recomposed. The arrow notation `(User@abc123 → User@def456)` shows the old and new identity hashes, confirming the value actually changed.
@@ -99,7 +140,7 @@ This is normal behavior. The parameter changed, so the composable recomposed to
D/Recomposition: └─ [param] onClick: () -> Unit stable (Function@xyz)
181
222
```
182
223
183
224
The `ProductCard` is now skippable. During scrolling, Compose will skip recomposing cards whose `product` and `onClick` values haven't changed, resulting in noticeably smoother performance.
0 commit comments