Jetpack Compose State Debugging with Rebugger

Oğuzhan Aslan
3 min readNov 16, 2023
Photo by Sigmund on Unsplash

State management lies at the core of Jetpack Compose development, dictating UI updates upon any state alteration. Mishandling state updates can lead to excessive UI refreshing, causing potential performance bottlenecks. Recognizing its criticality, the official documentation dedicates a section to state management (link).

While Android Studio’s “Layout Inspector” aids in visualizing recomposition counts, it falters with intricate states. Enter Rebugger, a powerful library designed to streamline state debugging by precisely pinpointing recomposition reasons.

Rebugger introduces a dedicated Rebugger Composable, facilitating easy logging of recomposition reasons. Its seamless integration simplifies the debugging process.

See the installation guide.

Imagine a scenario with a composable function like this:

@Composable
fun MainScreen(
text: String = "Hello World!",
onClick: (Int) -> Unit = {}
) {

val count = remember { mutableStateOf(0) }

Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Box(
modifier = Modifier.fillMaxSize()
) {
Button(
onClick = {
count.value++
onClick(count.value)
}
) {
Rebugger(
trackMap = mapOf(
"count" to count.value,
"text" to text
),
composableName = "MainScreen Button",
)

Text(text = "Hello World!" + count.value)
}
}
}
}

In this example, MainScreen holds two states: count updated by button clicks and text(may not be a direct state but a possible reason to recompose). If you run the composable above and click the button, you will see a log like this

MainScreen Button recomposed because 
`count` changed from `0` to `1`,

Rebugger can track not only state objects but also any object given in the trackMap. For instance, the example above has a text object in the trackMap. When the owner of the text value changes the text, the composable will be recomposed, and the recomposition reason will be logged. Let’s say you call the composable like this:

  RebuggerTrialTheme { // A surface container using the 'background' color from the theme
val text = remember { mutableStateOf("Hello, World!") }
MainScreen(
text = text.value,
onClick = { count ->
text.value = if (count % 2 == 0) {
randomString()
} else {
"Hello World!"
}
}
)
}

The text value is updated by the onClick callback. When the text is updated, the composable will be recomposed. And the recomposition reason will be logged like this:

MainScreen Button recomposed because 
`count` changed from `0` to `1`,
`text` changed from `Hello, World!` to `lorem`,

Note:

The Rebugger function gets the composable name from the stack trace of the caller, but sometimes this causes confusion. Thus, I suggest you pass a “composableName” any time you use Rebugger.

Additionally, Rebugger’s flexibility allows the utilization of alternative loggers like Timber by specifying the logger parameter.

Rebugger(
trackMap = mapOf(...),
composableName = "...",
logger = { tag, message -> Timber.tag(tag).d(message) }
)

In conclusion, Rebugger emerges as a fundamental tool, alleviating the complexity of state debugging within Jetpack Compose. Its intuitive implementation and versatile tracking capabilities offer developers a streamlined approach to pinpointing recomposition reasons, thereby enhancing app performance and development efficiency. Embrace Rebugger and elevate your Compose debugging experience!

--

--