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!

The Role of AndroidView

Google’s Jim Sproch wrote:

I’m always a little surprised when I see people using AndroidView directly in their apps, and maybe it’s because we made the API too pleasant to use or maybe it’s because we never really communicated the intent to the community. 🤷 Certainly I never expected to see people remembering the view and then just referencing that view in the viewBlock, I suppose no API survives first encounter with the user.

Composables are epitomized by their convenient declarative APIs. The older android Views often have imperative APIs with sometimes surprising semantics that need to be papered over, and this wallpaper isn’t always straight forward to get right. The intent/expectation was that users would write a composable function who’s sole purpose was to wrap a particular Android View, and would provide this wallpaper, such that the widget was pleasant to use in the rest of their application.

Basically, what Jim is expecting is a single-purpose composable, just to wrap the legacy View:

I would not expect AndroidView to ever have any siblings within a given widget. I would expect it to ALWAYS be of the form:

@Composable
fun EditText(text: String, onChange: (newValue: String)->Unit, ...) {
    AndroidView(
        viewBlock={
           android.widget.EditText(context)
            .apply {
                layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
            }
        },
        editBlock={
            // Deal with logic to add/remove change listeners, etc.
        }
    )
}

Now EditText looks like a composable function and has a declarative API, hiding the details of adding/updating change listeners, etc. It now provides an API that makes it look much more like a native composable.

There is an inherent impedance mismatch when crossing between declarative and imperative APIs. The intent was that people would deal with that impedance mismatch within a tightly-scoped composable that encapsulates/hides the ugly details, and would provide only an idiomatic declarative API to callers.

Jim’s concerns seem to stem from a separate thread, where a developer used an AndroidView() wrapper around an EditText subclass, directly in a more complex composable along with a Column() and Text() composables. Jim would prefer that the AndroidView() be isolated in its own composable, with that wrapper composable then used alongside “native” composables.

Various developers have already been working on this sort of concern. Zach Klippenstein wrote:

All our interop between Android views and compose is expected to happen at very well defined, explicit boundaries and I think we’ll probably try pretty hard to discourage any other integration. So whatever this api ends up being, we only need to wrap it in one place. I think the AndroidView api makes sense and pretty elegant, but I would probably be excited if you decided to make it higher-friction if that extra friction also made it harder to use incorrectly and helped reduce potential bugs. It would actually make my job easier.

…while Colton Idle is thinking more about tooling:

I’m thinking of writing a custom lint rule to prevent my team from using an AndroidView directly and guide them towards creating a wrapper composable.

Early on, there will be more use of AndroidView() than there will be over time, because:

Even in cases where the underlying implementation remains a View (e.g., WebView), we should wind up with official composable wrappers, and we can aim to migrate to those as they become available.

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


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


More!

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