One Off the Slack: Composable Lambdas as Factories

Bradleycorn asked:

I wrote a little abstraction to render some content (a series of Text composables) either horizontally (Row) or Vertically (Column).

Does Compose offer a better (built-in) way to do this?

enum class Orientation {
    HORIZONTAL,
    VERTICAL
}

@Composable
fun OrientedContent(
        orientation: Orientation, 
        modifier: Modifier = Modifier, 
        content: @Composable () -> Unit) = when (orientation) {
    Orientation.HORIZONTAL -> Row(modifier = modifier) { content() }
    Orientation.VERTICAL -> Column(modifier = modifier) { content () }
}

Google’s Adam Powell pointed out that the above code probably does not work in the manner that Bradleycorn expected. He offered this illustration of the problem:

var orientation by remember { mutableStateOf(Orientation.Horizontal) }

OrientedContent(orientation) {
  var counter by remember { mutableStateOf(0) }

  Button(
    onClick = {
      counter++
      orientation = if (orientation == Orientation.Horizontal) Orientation.Vertical else Orientation.Horizontal
    }
  ) {
    Text("Toggle!")
  }
  Text("Clicked $counter times")
}

on android counter is always 0, because each of the two calls to content in the different when branches has different identity

When we provide a composable lambda expression to another composable and invoke it, the lambda creates and adds a composable sub-tree to our existing hierarchy. However, that sub-tree is tied to the call site of the lambda expression, as part of the Compose compiler plugin magic:

anyway, key takeaway: composable lambda parameters are factories for content, and each call site of that lambda creates an instance of it

Bradleycorn wrote his own interpretation:

or in layman’s terms … if you write a composable that has a composable content lambda parameter, most of the time you probably want to avoid calling the content lambda in multiple places

Adam pointed out that mulitple calls to the lambda expression will be desired in some places:

unless you explicitly want multiple instances of that content, yes. (Think things like LazyColumn where the lambdas can represent the same presentation of different data.)

IOW, it is safe to invoke that composable lambda multiple times, so long as you know why you are doing it and are careful about the ramifications.

In truth, Bradleycorn’s original implementation holds up, if the supplied composable lambda has no state locally or downstream. We are going to wind up with a bunch of our own composables that work this way: they behave in simpler cases (e.g., has no state) and have problems in more complex cases (e.g., has state). And, due to the hierarchical nature of composables, a bug in a high-enough composable could cause problems only visible deep in the hierarchy, making debugging challenging.


Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!