One Off the Slack

These articles summarize the discussion on a particular thread in the #compose channel of JetBrains’ Slack workspace for Kotlin developers. If you would like to read the original thread, you will need access to that workspace — sign up at https://slack.kotlinlang.org!

Matching the Parent’s Size

Vsevolod Ganin asked:

I wonder why the matchParentSize modifier is available only in BoxScope? It could be useful in any layout scope I think, as well as matchParentWidth and matchParentHeight

The example use case was something like:

Column {
    Text(text = "4")
    Divider(modifier = Modifier.fillMaxWidth().preferredHeight(1.dp), color = Color.Black)
    Text(text = "2")
}

The objective is to have the Canvas() just take up the space used by the two Text() composables. Instead, fillMaxWidth() causes the Column() to want to take up the entire available width. And, since this is not in a Box(), matchParentWidth() is not an available modifier.

As Google’s Adam Powell pointed out, Box() has dedicated logic to support those modifiers:

it’s not available in every layout because it requires some special handling by the parent, and we don’t require that every custom layout perform that special handling.

matchParentSize in Box is an instruction to measure that element last, with whatever specific size the Box has already determined for itself, potentially from measuring its other child elements first.

Adam then pointed out an alternative: request a preferred width for the Column() using intrinsic size measurements. Timo Drick was able to use that to create a revised version of the sample:

Column(Modifier.preferredWidth(IntrinsicSize.Min)) {
    Text(text = "4")
    Divider(modifier = Modifier.fillMaxWidth().preferredHeight(1.dp), color = Color.Black)
    Text(text = "2")
}

Here, Timo used preferredWidth(IntrinsicSize.Min) to ask the Column() to not expand its width beyond the core requirements of the child composables. So, the Column() winds up with the width defined by the two Text() children — the fillMaxWidth() on the Divider() does not expand the Column() further.

Posted 2021-01-17, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1610804700138300


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.

Posted 2021-01-10, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1610210199359100


ViewModels… Going, Going, Gone?

Brady Aiello asked:

Since Composables will eventually replace Fragments, I was wondering if we’ll be able to scope ViewModels to Composables, or Composable destinations within Compose Navigation? Keeping lots of ViewModels all scoped to a single Activity, or keeping one god-ViewModel would both hold data for every page in memory concurrently, and that seems like a bad idea.

Google’s Ian Lake eventually chimed in with a definitive answer:

Navigation already scopes ViewModels to individual destinations and had since it’s first alpha

Along the way, though, Google’s Adam Powell pointed out a few key ideas. One is that the old activity destroy-and-recreate cycle is perhaps unnecessary:

if you’re using all-compose, there’s no reason to leave activity recreation for configuration changes enabled since compose knows how to handle them at a much finer granularity

Personally, while I think that this is a fine objective, I suspect that it may not turn out as well as Adam would like. If nothing else, it will require developers to pay very close attention to new types of configuration changes, as the android:configChanges attribute is a change blacklist. Any new type will trigger a configuration change, unless developers add it to android:configChanges. This will be easy to miss with the typical glacial pace of Android upgrades. Perhaps this can be handled via Lint.

Adam goes onto say:

which then means the arch components ViewModel superclass doesn’t serve much purpose; any old object you remember {} persists as normal

we have the savedInstanceState APIs in compose already for saving across process restarts, and that doesn’t experience the same ViewModel scoping questions

In other words, viewmodels may not go away, but ViewModel might not be the base class for them. We still are going to want something in that role, for testability if nothing else. But we might be able to avoid some of the ViewModel quirks.

Posted 2021-01-03, based on https://kotlinlang.slack.com/archives/CJLTWPH7S/p1608859219262500


More!

Older “One Off the Slack” articles can be found in the archives.