Blog post
This blogpost will mainly focus on Compose UI, the discussion about Compose Runtime/Compiler deserves a separate post/posts, so, for clarity, when the blog post mentions Jetpack Compose, we’re talking about Compose UI.
What is Jetpack Compose?
Jetpack Compose is a declarative toolkit for building native UI’s, which enables us to write UI code in Kotlin, removing the need for XML in the UI layer.
Why would we want to use this “new” solution?
- Kotlin - the entirety of Compose is written in Kotlin, with an emphasis on providing good/clean Kotlin APIs for us developers to consume. It eliminates the need to context switch between XML and Kotlin when writing your applications.
- Declarative - Compose takes a Declarative approach to build UIs such as React, Flutter, SwiftUI.. If we were to compare the current Android toolkit which takes an imperative approach, Compose solves some of the common issues that occur when dealing with imperative UIs.
Let’s imagine the following scenario: we have a very primitive LCE sealed class which represents our ViewState. To be clear, LCE isn’t the only way to model UI state, there are various implementations with their own upsides/downsides, but that’s outside the scope of this blogpost. Research each solution and see if it fits your project needs.
If we were to consume this viewState in our Activity with the imperative approach, a naive approach would look like this:
If we were to run this logic on an actual application, we’d quickly discover one major flaw.
Let’s imagine a very common scenario where we want to request some data from an API:
- We updated the Viewmodel viewState to Loading.
- Then an error occurs and we update the viewState to Error.
We’d surely expect to only show the error message, but with the above approach, we’d render something like this.
To our “surprise”, we render both the Loading state and the Error state.
Why is that?
Well, it’s simple. The “old” Android view system takes an imperative approach, and with that we have to mutate the existing view nodes to match our desired behaviour.
( by calling .setText, setVisiblity, etc etc …)
One quick observation would be that we actually have two sources of truth for our viewState.
- one in the ViewModel;
- one in the existing view nodes.
This is one of the main reasons why the UI doesn’t accurately reflect the actual data changes in Android, and that’s why we’re forced to write the actual UI code like this.
Now the question is, how does a declarative toolkit like Jetpack Compose solve this issue?
If we were to consume the same ViewModel state in Jetpack Compose, a bare bones implementation would look something like this:
At first glance we’d think that this implementation suffers from the same flaws as the old toolkit, that we’d render “outdated” data, and that we have to keep track of multiple sources of truth.
Fortunately for us, that is far from the truth. Just as we mentioned before, Jetpack Compose is a declarative toolkit. To put it simply,with Jetpack Compose (and any other declarative toolkit/framework) we provide the state and the underlying framework (in this case Compose Runtime/Compiler) and it will recompose the existing @Composable functions in the most effective way it can.
Let’s go through our previous example, but this time with the Compose implementation.
Initially, the viewModel will emit Loading state, the Compose Runtime/Compiler will be notified of this change since we’ve consumed the viewState as a Composable State (we’ll mention State and MutableState more in the next section), it’ll go through our logic and will build the LoadingIndicator() @Composable function. The other Composable functions won’t even be built, so there is no need to maintain their state, since they are never inflated (unlike the old imperative approach).
The API call is finished and now the viewmodel emits the Error state, the RepositoryScreen will again recompose, since it’s listening for changes on our viewState. It’ll again go through our logic and build the ErrorMessage() @Composable, notice that we don’t have to maintain the previous LoadingIndicator() state, since the Compose Runtime/Compiler won’t even build that @Composable.
We might think that this is wasteful, but the whole concept of declarative design is centered around recomposing existing components or adding new components. Generally, it’s a lot “cheaper” to recompose an @Composable than it is to inflate and view from the “old” toolkit.
Reactive by design
Reactive programming is nothing new in UI development.
Tools like RxJava, LiveData and Kotlin Flows have been around for quite some time, and have helped us bridge the gap to Reactive Programming.
Jetpack Compose is reactive by default, it relies on its own observable type called State<T>, or its mutable part MutableState<T>. Any underlying changes within a State object will trigger a recomposition of a @Composable function that consumes that state thanks to the Compose Runtime/Compiler.
One important caveat is the Compose Runtime/Compiler. It is smart enough to only recompose the minimal number of @Composable functions needed to reflect the State change. We can assist the Compiler by modelling our data in an immutable way or flagging it with the @Stable annotation.
Luckily for us, the Jetpack Compose team provides extension functions to convert from the most popular reactive frameworks to Composes State<T> object:
- subscribeAsState - bridges the gap from RxJava to Compose, it’s capable of consuming Observable, Flowable, Single, Maybe, Completable to State;
- observeAsState - bridges the gap from LiveData to Compose;
- collectAsState - bridges the gap from Kotlin Flow to Compose, it’s capable of consuming “regular” Flow’s and StateFlow to State.
Decoupled
Jetpack Compose isn’t tied to a particular Android OS version, and because of that, it can ship updates whenever there are any major/minor updates available.
We can remember the dark ages in Android development where everything was tied to the OS version and we’d have to wait 1+ year to get an update on one of the core functionalities, and thankfully the team at Jetpack Compose followed the best practices from the support/AndroidX libraries.
Backwards Compatible
Starting a new greenfield project is every developer’s dream, where we can fix our previous design mistakes and iterate on new ideas/APIs that weren’t available before. Sadly for us, there will always be legacy projects which require maintenance.
Thankfully, the Jetpack Compose team thought about this and provided us with multiple APIs to slowly start migrating the existing solutions into the declarative world.
ComposeView
a view which allows us to bridge the gap from the “old” toolkit to the Compose world, we can declare it in XML
or programmatically, if needed.
ComposeView is useful if we want to gradually migrate our code base from the “old” toolkit to Compose.
For instance, we might be using Jetpack Navigation with Fragments, and we don’t want to change our navigation logic just yet, instead we just add the ComposeView as the only view in our Fragments and write our @Composables there.
AbstractComposeView
is a useful tool if we, for instance, follow a view based architecture with lots of customViews/compundViews.
For instance, we can look at this example:
We’ve decided that rewriting the ToolBarView into Compose is relatively easy and we want to start there, so instead of declaring a ComposeView here and in every other place where com.my.app.ToolBarView is used, we can change the ToolBarView implementation to something like this:
With this change, now every implementation of ToolBarView will use Compose under the hood.
AndroidView
The compatibility doesn’t only go one way, Jetpack Compose is new (1.0.3 at the time of writing this blog) and it has a lot of catching up to do to provide all of the functionality that the “old” toolkit supplied.
Does that mean that we are forced to write a mix of XML and ComposeView and AbstractComposeView until Compose catches up ?
Luckily for us, we can add AndroidView to our @Composable functions and call views from the “old” toolkit directly.
Multiplatform
One additional selling point of adopting Compose into your codebase is the possibility of multiplatform development.
The Compose Runtime/Compiler isn’t tied to the Jetpack Compose UI (Android) implementation, the Compose Runtime/Compiler is a general purpose tool to manage a node (technically a tree of nodes) of any type.
With that in mind, the people at JetBrains are working on:
We’d have to tackle our architecture in a different way if we want to get the most out of the multiplatform approach, like avoiding Android ViewModels, ConstraintLayout and similar dependencies which are only tied to one platform, but that again deserves another blogpost.
Short Demo
With all that in mind, we can demonstrate how to build a basic topbar in Jetpack Compose with some basic animations.
We’ll demonstrate how to build this.
This short demo contains a simple screen containing three buttons and each will modify a local state in our @Composable function.
The topbar @Composable will listen to those changes and depending on the state changes, it’ll recompose and trigger some basic animations.
Let’s get started.
First, let’s define our model.
And our TopBarScreen @Composable which consists of:
- Scaffold - we can think of it as a blueprint for implementing Material Design which comes with opinionated “slots” for topBar, floatingActionButton, bottomNav etc…
- Column - we can think of it as a LinearLayout with the orientation set to vertical.
- Three Buttons - that contain a simple Text @Composable as their body, and in the onClick lambda we just simply modify the state.
And finally, our AnimatedTopBar @Composable, which is a simple TopAppBar implementation, but the only two interesting changes are:
- animateContentSize modifier - a really useful modifier which will animate the size of a composable, depending on the changes inside. In our case, if, for instance, subTitle is null when the state get’s updated, we’ll get a nice animation for “free”.
- AnimatedVisiblity @Composable - just as the name suggests, a composable with the responsibility to handle appearance and disappearance of its content. To trigger the visibility logic, we need to provide a boolean value in the visible parameter and we have the ability to customize the enter/exit animations with the enter/exit parameters.
Conclusion
Jetpack Compose is a huge paradigm shift for Android developers. Exploring and modifying the already implemented Components/APIs has never been easier on Android.
With its first class Kotlin support and intuitive design, Jetpack Compose is the future of Android development.