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!