Jetpack Compose: What you need to know, pt. 2
Tags: android
Published: 2021-03-18 16:08:59

This is the second and final part of the Jetpack Compose series that combines curious excitement with a healthy dose of cautious skepticism. Let’s go!

Ecosystem
Official documentation doesn’t cover enough.

That’s understandable in this phase of development, but it absolutely needs to be significantly expanded before Compose hits 1.0.

On top of that, Google is once again getting into the bad habits of 1) mistaking developer marketing for advocacy and 2) scattering useful bits of information between official docs, KDoc, semi-official blogs, code samples, or other sources with unknown relevance. Although these can be useful, they’re difficult to find and are not usually kept up-to-date.

Interoperability is good.

We can use legacy Views in our Compose hierarchy and composables as parts of View-based UIs. It works, we can migrate our UIs gradually. This feature is also important in the long term, as I wouldn’t expect a Compose version of WebView or MapView written from scratch any time soon, if ever.

Compose also plays nicely with other libraries—it integrates well with Jetpack ViewModel, Navigation, or reactive streams (LiveData, RxJava, or Kotlin Flow—StateFlow is especially well suited for the role of a stream of states coming from the view model to the root composable). Popular 3rd party libraries such as Koin also have support for Compose.

Compose also gives us additional options. Its simplicity allows for much. For example, it is very well possible to completely get rid of fragments and/or Jetpack Navigation (although in this case, I think one vital piece of the puzzle is still missing—our DI frameworks need the ability to create scopes tied to composable functions), but of course you don’t have to. Choose what’s best for your app.

All in all, the future of the Compose ecosystem certainly looks bright.

Tooling is a work in progress, but the fundamentals are already done.

Compose alphas basically require canary builds of Android studio, which are expected to be a little bit unstable and buggy. Nevertheless, specifically for Compose, the Android tooling team has already added custom syntax and error highlighting for composable functions, a bunch of live templates, editor intentions, inspections, file templates, and even color previews in the gutter (Compose has its own color type).

Compose also supports layout previews in the IDE, but these are more cumbersome than their XML counterparts. A true hot reload doesn’t seem to be possible at the moment.

The IDE also sometimes struggles when a larger file with lots of deeply nested composable functions is opened in the editor. That said, the tooling won’t hinder your progress in a significant way.

UI testing is perhaps more complicated than it was with the legacy toolkit.

In Compose, there are no objects with properties in the traditional sense, so to facilitate UI tests, Compose (mis)uses its accessibility framework to expose information to the tests.

To be honest, it all feels a little bit hacky, but at least we have support for running the tests on JUnit 4 platform (with the help of a custom rule), Espresso-like APIs for selecting nodes and asserting things on them, and a helper function to print the UI tree to the console.

The situation is thus fairly similar to the legacy toolkit, and so is my advice: Mind the test pyramid, don’t rely too much on UI tests, and structure your app in such a way that the majority of the code can be tested by simple unit tests executed on the JVM.

Performance and stability
Build speeds can be surprising.

In a good way! One would think that adding an additional compiler to the build pipeline would slow things down (and on its own, it would), but Compose replaces the legacy XML layout system, which has its own performance penalties (parsing XMLs, compiling them as resources, etc.).

It turns out, even now when Compose is still in a very early stage of development, the build time of a project written with Compose is at least comparable to the legacy UI toolkit version—and it might be even faster, as measured here.

Runtime performance is a mixed bag.

UIs made with Compose can be laggy sometimes, but this is totally expected since we are still in alpha. Further optimizations are promised down the line, and because Compose doesn’t come with the burden of tens of thousands of LOC full of compatibility hacks and workarounds in each component, I hope someday Compose will actually be faster than the legacy toolkit.

It crashes (it’s an alpha, I know).

In my experience, Compose crashes both at compile time (the compiler plugin) and at runtime (usually because of a corruption of Compose’s internal data structure called “slot table”, especially when animations are involved). When it does crash, it leaves behind a very, very long stack trace that is full of synthetic methods, and which is usually also totally unhelpful.

We definitely need special debugging facilities for Compose (similar to what coroutines have), and yes, I know, the majority of these bugs will be ironed out before 1.0. The thing is, Compose simply must be reliable and trustworthy at runtime because we are not used to hard crashes from our UI toolkit—for many teams, that would be an adoption blocker.

Expectations
Compose is meant to be the primary UI toolkit on Android.

Several Googlers confirmed that if nothing catastrophic happens, this is the plan. Of course, it will take years, and as always, it won’t be smooth sailing all the way, but Google and JetBrains are investing heavily in Compose.

Compose is no silver bullet.

Yes, Compose in many ways simplifies UI implementation and alleviates a significant amount of painful points of the legacy UI toolkit.

At the same time, it’s still possible to repeat some horrible old mistakes regarding Android’s lifecycle (after all, your root composable must still live in some activity, fragment, or view), make a huge untestable and unmaintainable mess eerily similar to the situation when the whole application is written in one single Activity, or even invent completely new and deadly mistakes.

Compose is not an architecture. Compose is just a UI framework and as such it must be isolated behind strict borders.

Best practices need to emerge.

Compose is architecture-agnostic. It is well suited to clean architecture with MVVM, but that certainly isn’t the only possible approach, as it’s evident from the official samples repo. However, in the past, certain ideas proved themselves better than others, and we should think very carefully about those lessons and our current choices.

Just because these are official samples by Google (or by anyone else for that matter), that doesn’t mean you should copy them blindly. We are all new to this thing and as a community, we need to explore the possibilities before we arrive at a set of reasonable, reliable, and tried-and-proven best practices.

Just because we can do something doesn’t mean we should.

There are a lot of open questions.

The aforementioned official samples showcase a variety of approaches, but in my book, some are a little bit arguable or plainly wrong. For example, ask yourself:

How should the state be transformed while passed through the tree, if ever? How should internal and external states be handled? How smart should the composable functions be? Should a view model be available to any composable function directly? And what about repositories? Should composable functions have their own DI mechanism? Should composable functions know about navigation? And data formatting, or localization? Should they handle the process death themselves? The list goes on.

Should you use it in production?

Well, it entirely depends on your project. There are several important factors to consider:

Being still in alpha, the APIs will change, sometimes significantly. Can you afford to rewrite big parts of your UI, perhaps several times?
There are features missing. This situation will get better over time, but what you need now matters the most.
Runtime stability might be an issue. You can work around some things, but there’s no denying that Compose right now is less stable than the legacy toolkit.
What is the lifespan of your application? If you’re starting an app from scratch next week, with plans to release v1.0 in 2022 and support it for 5 years, then Compose might be a smart bet. Another good use might be for proof of concept apps or prototypes. But should you rewrite all your existing apps in Compose right now? Probably not.
As always with new technology, all these questions lead us to these: Are you an early adopter? Can you afford to be?

Under the hood
Compose is very cutting edge (and in certain aspects quite similar to how coroutines work).

In an ideal world, no matter how deeply composable functions were nested and how complex they were, we could call them all on each and every frame (that’s 16 milliseconds on 60 FPS displays, but faster displays are becoming more prevalent). However, hardware limitations of real world devices make that infeasible, so Compose has to resort to some very intricate optimizations. At the same time, Compose needs to maintain an illusion of simple nested function calls for us developers.

Together, these two requirements result in a technical solution that’s as radical as it’s powerful—changing language semantics with a custom Kotlin compiler plugin.

Compose compiler and runtime are actually very interesting, general-purpose tools.

Kotlin functions annotated with @Composable behave very differently to normal ones (as it’s the case with suspending functions). This is possible thanks to the IR code being generated for them by the compiler (Compose uses the Kotlin IR compiler backend, which itself is in alpha).

Compose compiler tracks input argument changes, inner states, and other stuff in an internal data structure called slot table, with the intention to execute only the necessary composable functions when the need arises (in fact, composable functions can be executed in any order, in parallel, or even not at all).

As it turns out, there are other use cases when this is very useful—composing and rendering UI trees is just one of them. Compose compiler and runtime can be used for any programming task where working efficiently with tree data structures is important.

Compose is the first big sneak peek at Kotlin’s exciting future regarding compiler plugins.

Kotlin compiler plugins are still very experimental, with the API being unstable and mostly undocumented (if you’re interested in the details, read this blog series before it becomes obsolete), but eventually the technology will mature—and when it does, something very interesting will happen: Kotlin will become a language with more or less stable, fixed syntax, and vastly changeable, explicitly pluggable behavior.

Just look at what we have at our disposal even now, when the technology is in its infancy: There is Compose, of course (with a desktop port in the works), a plugin to make classes open to play nice with certain frameworks or tests, Parcelable generator for Android, or exhaustive when for statements, with more plugins coming in the future.

Last but not least, I think that the possibility to modify the language with external, independent plugins will lower the pressure on language designers, reducing the risk of bloating the language—when part of the community demands some controversial feature, why not test-drive it in the form of a compiler plugin first?

Final words
Well, there you have it—I hope this series helped you to create an image of Compose in your head that is a little bit sharper than the one you had before. Compose is certainly going to be an exciting ride!

Written by: Jiri Hutarek @jhutarek