Background of Feature Request
Composables are developed as reusable functions, allowing a single composable to be used in multiple places. In the given Kotlin code structure:
@Composable fun House() {
Content()
}
@Composable fun Door() {
Content()
}
@Composable fun Content() {
Text("It's me!")
}
When both House
and Door
composables recompose, the Content
composable is also recomposed. However, the Content
composable has no knowledge beyond being recomposed; it doesn't know which parent triggered the recomposition. For effective debugging, it is crucial to know which parent caused the recomposition of the Content
composable. Currently, developers have to inspect both House
and Door
composables to identify the source of unintended recomposition.
Proposed Implementation Approach
The idea is to wrap all composable function calls with a try-finally
block to record the call stack. For instance:
@Composable fun House() {
try {
// Enter `Content()`
Content()
} finally {
// Exit `Content()`
}
}
Extending this approach to wrap all composable function calls in a try-finally
block and record the call stack could provide valuable debugging information.
Implementation Considerations
Composable lambdas that do not capture values are managed by the ComposableSingleton
class, while lambdas that capture values are wrapped in the ComposableLambda
class. Therefore, unintentionally, the ComposableSingleton
or ComposableLambda
classes may be present in the Compose function call stack. To prevent this, recording the call stack should be performed before the Compose compiler.
Assistance Needed
I attempted to implement this quickly in pull request #78, and while the try-finally
concept worked, there were issues. Composables can be defined not only as functions but also as anonymous functions. For example:
@Composable fun Content(content: @Composable () -> Unit) {
content()
}
@Composable fun ActualContent() {
Content {
Text("beep bop boop")
}
}
The expected call stack for Text("beep bop boop")
would be ActaualContent -> Content -> Text
. However, in reality, an <anonymous>
layer is introduced because Text("beep bop boop")
is created from an anonymous composable function.
@Composable fun ActualContent() {
Content(
content = @Composable {
Text("beep bop boop")
}
)
}
Replacing <anonymous>
with the parameter name content
provides a meaningful name, but is this approach always valid?
For now, I'm creating this issue to document my current situation and will continue to develop this way.