A Brief History of Compose

Compose is now a few versions into RC, and is going to be released fully later this month – meaning that all public API is now stable. Here’s a brief look at how this came about.

In about 2018, a small team within Google started working on a new UI project. Initially it was just to create a declarative UI framework, but the more it was worked on, the more features they realized that could be completely overhauled.

Compose is a complete (almost) rewrite of the UI components Android uses, It doesn’t use an Android TextView when you use the Text Composable. It’s not using a LinearLayout when you use Column. This is in contrast to SwiftUI, where depending on the platform you target, will potentially use the existing components from UIKit (when targeting iOS). So why?

Going all the way back, the project was initially just to create a method of writing UI in a declarative way. The very early versions of Compose were written in XML. These used the current UI toolkit (so TextView for example) but the XML you’d write would be a description of how the view should look with a given data item. Side-note: That XML was actually written inside Kotlin classes for some reason, so Kotlin files would have this weird XML hacked into it.

They eventually realized, from multiple sources (DataBinding; SwiftUI; thinking) that perhaps pure Kotlin code was the way to go. So they scrapped XML and went with Kotlin. Composables in Kotlin were all classes. If you’re unfamiliar with SwiftUI, it has three main functions that allow it to return types that represent views:

  1. Generics inside a protocol (interface) don’t require you to specify what the generic type is in certain circumstances. So for example, in Kotlin, let’s say I declared an interface like this:

interface View<T>

If I had a method or computed property that returned an instance of that View interface, what would I put for its generic type?

Swift introduced what’s called opaque return types, which allow you to return some protocol. This is why in SwiftUI, you’re always returning some View without specifying the generic anywhere.

  1. Type inference. The compiler can figure out what the end result type of your body property is and so you don’t have to specify it. This is hugely advantageous for SwiftUI, since most modifiers will wrap your previous view in another View type, like _BackgroundModifier. By having the compiler figure out the type, I don’t have to worry about specifying it.
  2. Value types. Swift has stricts, which are value types that do not (yet) exist in Kotlin. They will eventually be extended, but right now value types in Kotlin can only accept one property.

Kotlin can’t copy that any of the above. Java bytecode has its types erased, and the Kotlin language forces you to specify the generic parameter when you declare that a function returns an interface or class. So to get around this, this now growing team decided to go with using classes everywhere.

That was fine for a bit, but it became clear through usage that it wasn’t the best way. Classes inherently come with many functions for free that you might not want. What they saw people doing was throwing exceptions inside a free function that they didn’t want consumers to invoke. So in essence, this API design forced consumers to restrict the functionality of a class.

Instead they switched to functions, for two reasons. First is the above – restricting the functionality of a class resulted in crashes all over the place, and second is that if they were to continue down this road of using classes, they’re stuck. It’s much more difficult to remove classes later on than it would be to add classes if there were a viable reason. So instead they went with extending the functionality of functions.

Why is this is a completely new UI toolkit?

The existing one is fine, right? Button extends TextView – why? ImageButton extends ImageView – why doesn’t it extend Button?? View.java is over 30,000 lines of code now. Everything is written in Java. So mainly, they realized that it’s an outdated toolkit, and they treated Compose as an opportunity to write a new UI toolkit that will last the next 10 years. Rewriting everything with all the knowledge they gained about where the old toolkit failed made sense. Also, since Kotlin now has coroutines (async/await), Composables can be created off the main thread, resulting in huge performance improvements.

The minimum API level of Compose

Compose supports API 21 and up. They wanted to go back further, but there were two main reason why 21 was targeted:

  1. ART became the default runtime, which had optimizations that the previous runtime didn’t have, like a better garbage collector for memory optimization.
  2. New platform behaviour introduced here, such as the render thread, made it easier to implement things like animations.

Is this an annotation processor?

Even though you annotate a Composable function with @Composable, it doesn’t use an annotation processor anywhere. It’s just a compiler plugin. That plugin will change the signature of a function when @Composable is attached to it, much in the same way as the suspend keyword changes a function type. But it doesn’t make sense for Kotlin, a language also used on servers, to introduce a keyword that’s exclusive to UI applications. So the option they went for was to introduce this annotation, search for uses of it in their compiler plugin, and strip it away. At runtime, Composables like Text are just drawing directly onto Canvas – the same Canvas object that currently exists. This is why so many widgets aren’t yet available – they have 10 years worth of UI widgets to re-write.

Why do Composable functions use upper-pascal case?

Remember when member variables were prefixed with m? This is the same sort of idea. Your code will still compile if you use lower-pascal function names – though the IDE will show you it’s not happy with your choice. The team decided to use this upper-pascal because of confusion they saw when Composables were named in lower-pascal case. Typically when you see a function, you expect it to start, do its job, and then on the next line down, that function will be finished. But that’s not true with Compose – when a Composable is invoked, it is still there on the call graph in memory. Having the naming convention use upper-pascal is their attempt to make that more obvious in PRs.

Why does auto-complete suck?

Kotlin itself isn’t too bad with auto-completion of code. But for some reason, Compose sucks. There is a reason for this, and it’s one where the benefit you get is unfortunately worth the trade-off of poor auto-completion. Kotlin has a KProperty<T> type, where, when you use a delegate function, that KProperty would get allocated resources in memory. Now if you have a bunch of Composables all on screen, all using delegate functions for their remember invocations, a lot of resources are wasted on allocation of these unused KProperty instances.

It turns out that there’s this compiler trick where if you define a function as an inline extension function, instead of within the class definition, the KProperty won’t be allocated resources if it’s not being used. Since they wanted to push usage of delegate functions (var state by remember { ... }), they felt this optimization was worth the temporary annoyance.

What is this Modifier thing?

This is a huge difference between Compose and SwiftUI. When you attach a modifier to a View in SwiftUI, (most of the time) you’re changing the type. So if you define a Button and then add a .cornerRadius modifier – you now have a ModifiedContent. This is how the UI tree is built up, and you’re changing types as you add modifiers. Compose doesn’t work like this. If you create a Button and add a .clip modifier – you’re still dealing with a Button. No matter what modifiers you add, you’re always working with the originally defined view type. Adding a border to any Composable just draws some pixels around it when the whole thing gets drawn on screen, folding the modifiers you’ve added.

Initially, the API for this was that every modifier was itself a Composable function. However, quite quickly they realized this was the wrong choice. Look at this:

Padding(8.dp) { Text(“Hi”) }

Sort of obvious what that’s gonna do, right? Now what about this:

Padding(8.dp) { Text(“Hi”) Text(“Goodbye”) }

That Padding Composable now has two children. What’s the layout policy? Should it lay them out vertically, horizontally, on top of each other? Dunno. There were two other issues with this:

  1. Nesting – imagine if you want padding, fillMaxWidth, heigh of 300.dp – you’re already 3 layers deep and you haven’t even drawn anything.
  2. When new Modifiers were added with this API, if whoever was writing it didn’t want there to be more than 1 child, they’re throw exceptions. There’s no compile-time way of enforcing the number of children a Composable has since it’s just a function. So Modifier was the solution to this: a chain-style API where you don’t have to decide a layout policy.

This Modifier object also introduced a new piece of weirdness that still exists now. Take a look at this:

Text( text = “Hi”, modifier = Modifer .padding(4.dp) .rotate(45f) .border(Color.Blue) .background(Color.Red) .padding(16.dp) )

How are you reading that? Noting that the order of modifiers matter in the same way they do with SwiftUI, if you read that top-down, you’re going to get a very unexpected surprise. Modifiers in Compose need to be read bottom-up to understand what’s going on. The reason is a fundamental annoyance with the Compose API – every Composable is a function. They all return nothing. With SwiftUI, you can write a new modifier by extending the View protocol. But Composables have no type to extend, so instead each one needs to be passed a modifier into its function signature, and use that as the starting point of the chain. If you think about the child-parent relationship, this will mean that a parent can add new modifiers to a Composable child, but the child will still keep its own order of modifiers as expected.

@Composable fun ModifierExample() { Box { Greeting(modifier = Modifier.background(Color.Blue)) } } @Composable fun Greeting(modifier: Modifier = Modifier) { Text(“Hi”, modifier = modifier.padding()) }

Closing Thoughts

This API is by no means perfect. And this modifier API also has some of the same issues with SwiftUI – if I attach a scrollable modifier to a child which itself has a scrollable modifier to do its own work – what happens? Which modifier gets the callback? Currently, since Compose is in Beta, this can crash. The idea is that you can attach any modifier to any child without having to know its implementation and your modifiers will work.

Using Compose, once you wrap your head around it, is certainly far better than the current UI toolkit. Even if you only care about UI development, the new toolkit unlocks a lot of powerful features that are very challenging wit

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s