Recomposition in Jetpack Compose is a posh matter. It’s complicated as a result of typically you haven’t any concept why a sure perform is recomposed, which isn’t what you anticipated primarily based in your data. Thus, you’ll want to debug it.
Breakpoints in Debugger
Utilizing breakpoints in a debugger first involves my thoughts to debug recomposition. Nonetheless, there are a number of limitations to this strategy.
Commonplace Logging
So to debug with logging, you employ Log.d
. It seems like this
Log.d("DebugRecomposition", "RecompositionExample() perform scope")
Nevertheless it missed one essential piece of knowledge, which does not inform the present recompose scope info. This info is essential as a result of completely different composable capabilities can nonetheless have the identical recompose scope – see the reason beneath.
To print this recompose scope info, you employ $currentRecomposeScope
. Now the logging seems like this
Log.d("DebugRecomposition", "RecompositionExample() perform scope $currentRecomposeScope")
The log output seems like this:
D/DebugRecomposition: RecompositionExample() perform androidx.compose.runtime.RecomposeScopeImpl@894fab8
This RecomposeScopeImpl@894fab8
is the distinctive ID for this recompose scope. If one other composable perform has the identical distinctive ID, it means it additionally belongs to this identical recompose scope.
Properly, there’s nonetheless one lacking piece of knowledge – the recomposition depend. Technically, you continue to can manually depend the log assertion, however that may be very troublesome and vulnerable to error. Due to that, you want customized logging.
Customized Logging
I steal the customized logging code from this superb submit about recomposition right here, and I make a number of modifications to it as a result of I feel some stuff is simply pointless. Right here is the modified model.
class RecompositionCounter(var worth: Int)
@Composable
inline enjoyable LogCompositions(tag: String, msg: String) {
if (BuildConfig.DEBUG) {
val recompositionCounter = keep in mind { RecompositionCounter(0) }
Log.d(tag, "$msg ${recompositionCounter.worth} $currentRecomposeScope")
recompositionCounter.worth++
}
}
-
I renamed class
Ref
to classRecompositionCounter
to higher mirror it’s the recomposition depend -
I eliminated
SideEffect {}
and moved the counter increment after the logging. I don’t assume we wantSideEffect {}
right here. -
I added
$currentRecomposeScope
as further info which I feel is essential.
The
inline
is to make sure the mum or dad who calls this composable perform has the identical composable perform scope. So as phrases, when a mum or dad is recomposed, thisLogCompositions()
perform undoubtedly shall be referred to as.
Examples
Let us take a look at a easy instance beneath.
@Composable
enjoyable RecompositionExample() {
var depend by keep in mind { mutableStateOf(0) }
LogCompositions("DebugRecomposition", "RecompositionExample() perform scope")
Column {
LogCompositions("DebugRecomposition", "Column() content material scope")
MyButton(onClick = { depend++ }, textual content = depend.toString())
}
}
@Composable
enjoyable MyButton(
onClick: () -> Unit,
textual content: String) {
LogCompositions("DebugRecomposition", "MyButton() perform")
Button(onClick = onClick) {
LogCompositions("DebugRecomposition", "Button() content material")
Textual content(
textual content = textual content,
)
}
}
The log output seems like this throughout start-up:
D/DebugRecomposition: RecompositionExample() perform scope 0 androidx.compose.runtime.RecomposeScopeImpl@894fab8
D/DebugRecomposition: Column() content material scope 0 androidx.compose.runtime.RecomposeScopeImpl@894fab8
D/DebugRecomposition: MyButton() perform 0 androidx.compose.runtime.RecomposeScopeImpl@399bf6
D/DebugRecomposition: Button() content material 0 androidx.compose.runtime.RecomposeScopeImpl@dc1e8e2
- You discover the
RecompositionExample()
andColumn()
have the identical recompose scope. It’s because frequent layouts equivalent toColumn()
,Row()
, andField()
are all “inline” composable capabilities. Thus, they’ve the SAME recompose scope as their callers.
Should you click on the button, the log output seems like this:
D/DebugRecomposition: RecompositionExample() perform scope 1 androidx.compose.runtime.RecomposeScopeImpl@894fab8
D/DebugRecomposition: Column() content material scope 1 androidx.compose.runtime.RecomposeScopeImpl@894fab8
D/DebugRecomposition: MyButton() perform 1 androidx.compose.runtime.RecomposeScopeImpl@399bf6
D/DebugRecomposition: Button() content material 1 androidx.compose.runtime.RecomposeScopeImpl@dc1e8e2
-
When the button is clicked, the
depend
state is mutated. Thus, all to recompose scopes that learn the state shall be recomposed. -
In
column()
scope, it reads thedepend
state fromtextual content = depend.toString()
. Thus,column()
is recomposed. As a result ofcolumn()
andRecompositionExample()
has the identical recompose scope,RecompositionExample()
is recomposed as nicely. -
MyButton()
is recomposed as a result of the enter parametertextual content
is modified. Scope that learn thetextual content
shall be recomposed. Thus,Button()
andTextual content()
are recomposed too. There isn’t a logging forTextual content()
, so it would not present up within the log.
Conclusion
As talked about, recompose is a posh matter. This text would not give attention to why and the way recomposition can occur. It covers a bit within the examples above, however it’s only a pretty primary demonstration.
This text exhibits how one can debug it utilizing the customized logging LogCompositions()
to determine how recomposition behaves. In my view, recomposition is crucial idea to grasp in Jetpack Compose, understanding the way it works is essential.
Supply Code
GitHub Repository: Demo_UnderstandComposeConcept