1.
Developing UI in Android
Written by Denis Buketa
The user interface (UI) is the embodiment of your mobile app. You could say that it’s an ever-evolving relationship between a user and the system they interact with. When you look at the big picture, it’s easy to understand why UI design is so important: It’s the reason products either fail or succeed.
In this chapter, you’ll learn about the design concepts of the existing Android UI toolkit. You’ll review the basic building blocks, how to make custom views, the process to display the layout on your screen and the principles the current toolkit is built upon. You’ll learn about the reasoning behind these concepts, their drawbacks, how they evolved and how they influenced the idea behind Jetpack Compose — or Compose, for short.
After that, you’ll learn about Compose — the awesome new UI toolkit for Android, which is making all the Android kids super hyped! :]
You’ll see how Compose approaches each of the concepts of the current Android UI toolkit and why it’s the next evolutionary step in Android development.
Unwrapping the Android UI toolkit
In Android, you build your UI as a hierarchy of layouts and widgets. In code, layouts are represented by ViewGroup
objects. They are containers that control the position and behavior of their children on the screen.
On the other hand, widgets are represented by View
objects. They display individual UI components like buttons and text boxes.
As you can see in the image, you define each screen in Android as a tree of ViewGroup
and View
objects. ViewGroup
s can contain other ViewGroup
s and View
s. If you’re familiar with computer science structures, you’ll recognize that ViewGroup
s are like nodes of the tree structure, where each View
is a leaf.
The most important thing to notice here is that your View
objects are responsible for the look of your UI. So it makes sense to begin this journey by looking at how you implement and use the View
class.
View
As mentioned before, a View
represents the basic building block for UI components. It occupies a rectangular area on the screen where it draws the specific UI component, like a button or a text field.
But user interfaces aren’t designed to be static — at least, most of them aren’t. Your users want to interact with the UI by clicking, dragging or typing into it.
Fortunately, View
s also support this type of interaction and events. Specialized View
s often expose a specific set of listeners that you use to manage interactive events.
As you know, with great power comes great responsibility, and the Android View
component is definitely powerful. Every UI component you’ve ever used is a direct or indirect subclass of View
.
As Android grew as a platform, View
got bigger and bigger. In the current API, the View.java file has over 29,000 lines of code. Don’t believe it? Open it and check it by yourself! :]
This means that the current Android UI toolkit scales poorly and is increasingly harder to maintain.
Imagine yourself trying to fix a specific bug in the View
class file. Every small change you make in the base View
will reflect in who knows how many ways on the entire Android UI toolkit! You could be fixing one small bug, but at the same time creating dozens or hundreds of others!
View
is beyond the point of refactoring, yet the only way for the current Android UI toolkit to evolve is to make this class even bigger. And this issue is amplified when you try to build custom Views.
Implementing custom views
Despite the functionality provided by View
and the other custom widgets the current API offers, there are cases where you need to create custom views to solve specific problems.
Given how the entire UI toolkit is built on top of the View
, you’d think it would be very easy to build something custom, that extends the View
as its parent?
Well, think again! If you wanted to build even the simplest of custom View
s, you’d have to go through all of these steps:
class MyWidget : View {
// 1 - Overriding constructors
...
// 2 - Inflating layout
...
// 3 - Parsing attributes
...
// 4 - Getters
...
// 5 - Setters
...
// 6 - Measuring and Layout
...
// 7 - Handling touch events
...
}
The first thing you have to do is to create a class that extends from View
. Writing custom views to solve a particular problem is hard. There are a lot of things that you need to do right:
- You have to override the
View
constructors. Yes, there are multiple, each with its own use case! - To inflate the specific layout, you have to define it as an XML resource.
- To customize your
View
from XML, you have to create special XML attributes and add them to the attrs.xml file. - To modify your custom widget, you have to add all the necessary properties and their respective getters and setters to the class.
- You have to think about styles and how your
View
behaves in different display modes, such as light and dark theme. - If you need custom measurements or layouts, you have to override the specific callbacks.
- Do you need to handle touch events? Then you need extra code to add touch & gesture support!
As you can see, there are a lot of things that you need to think about when writing custom views. And as a developer, you probably want a clean and easy API that you can easily expand with your custom implementation. Unfortunately, the current Android UI toolkit is anything but easy.
ViewGroup
After all the work you’ve put into implementing your custom view, it’s time to add it to your UI. But before you do, you have to choose the correct ViewGroup
for your container. And that might not be such an easy decision. You’ll probably end up with more than one. When creating a layout for your screen, you always have to choose which ViewGroup
to use as your root view.
There are many different types of ViewGroup
s in Android. Some common implementations are LinearLayout
, RelativeLayout
and FrameLayout
. Each of these exposes a different set of parameters you can use to arrange their children:
- LinearLayout: Use this when you want to organize children in a row or column.
- RelativeLayout: Enables you to specify the location of child objects relative to each other or to the parent.
-
FrameLayout: One of the simplest containers, usually used to store one widget or
Fragment
.
However, UIs are not always so simple that you can use just one ViewGroup
in your layouts. When building more complicated UIs, you often define different areas of the screen and use the specific ViewGroup
that best matches your use case.
Usually, that leads to a lot of nested ViewGroup
s, which makes your code hard to read and maintain. Most importantly, it decreases the performance of your app.
Recently, the Android UI toolkit received a new ViewGroup
, to address this issue — ConstraintLayout
. It allows you to create large and complex layouts with a flat view hierarchy. In short, you use it to create complex constraints between views so you don’t have to nest so many layouts. Each constraint describes if a View
is constrainted to the start, end, top or bottom of another View
.
However this doesn’t quite solve the problem of nested layouts. There are times when you can still get better performance by combining simpler ViewGroup
s rather than using ConstraintLayout
.
Also, you could argue that sometimes in complex UIs, it’s easier to understand how the layout is organized when you have some level of nesting. Ironic, right? :]
As you can see, it’s not easy to pick a ViewGroup
when building your UIs. Usually, it becomes easier as you gain experience, but new developers still have a hard time when they start playing with Android.
Displaying views
Now, imagine you’ve successfully created your layout. You picked just the right View
s. You created a custom View
to solve a specific problem. You used the correct ViewGroup
s to organize your View
s. Now, you want to display your beautiful layout.
But there are still many more steps you need to take to achieve this behavior!
As you probably have experience with the Android UI toolkit, you know that most of your UI is defined in XML files. Android provides an XML schema for including ViewGroup
and View
classes. So, your layout might look like this:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World"
android:layout_gravity="center"/>
</FrameLayout>
This simple layout displays a text saying “Hello World”.
Cool! But all this work still isn’t enough to display your UI. To render that layout on the screen, you need to connect it with your Activity
or Fragment
. If you’re working with an Activity
, you’d do something like this:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_layout)
}
}
Using setContentView(ViewResource)
, the Activity
takes care of creating a window where you place your UI. This is the simplest way you can display UI elements in Android.
Fragment
s, on the other hand, represent a piece of behavior or a portion of the UI within an Activity
. You can combine multiple Fragment
s in a single Activity
to build a multi-pane UI, and you can also reuse a Fragment
in multiple activities. Think of a Fragment
as a modular section of an Activity
.
class MyFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return inflater.inflate(R.layout.layout, container, false)
}
}
To provide a layout for a fragment, you must implement onCreateView()
. The Android system calls it when it’s time for the fragment to draw its UI.
Your implementation of this method must return a View
that’s the root of your Fragment
’s layout. To return a layout from onCreateView()
, you inflate it from a layout resource you define in XML. To help you do so, onCreateView()
provides a LayoutInflater
— a special component that reads the XML definition, and builds Kotlin and Java objects using the attributes and properties you define, such as the View
width, height, color, constraints and custom attributes.
This process is also called layout inflation, as layouts are similar to baloons — you need to inflate them to take shape and do some work! :]
Imagine now, that you you need a screen with an Activity
and a Fragment
inside it. To create that screen, you’d need the following files: MyActivity.kt, my_activity.xml, MyFragment.kt, my_fragment.xml, extra attributes defined in attrs.xml and special styling defined in styles.xml.
For such a simple screen, you have to write too much code.
As you see, the current Android UI Toolkit scales very poorly. Modern applications usually have dozens, if not hundreds, of features each with its own XML layout, attributes, styles, Kotlin or Java code and much more.
On top of that, it’s impossible to organize those files in the resource folder. And if you decide to define list or page items for dynamic UI components, things get even more complicated.
Separating concerns
It’s hard enough to get from a simple layout definition to displaying your UI on the screen, but if you really want to see your UI in action, you have to connect it to your business logic.
You always hear that you should separate your business logic from your UI logic. This concept is known as separation of concerns (SoC). It’s a design principle in computer science that says you should separate a program into distinct sections, with each section addressing a distinct concern.
A concern is a set of information that affects the code of a computer program. It can be as general as the details of the hardware that runs an app or as specific as the name of the class to instantiate.
SoC includes two main concepts: coupling and cohesion. You can think of your app as a group of modules. Each module contains many units. In general, you want to reduce coupling as much as possible and increase cohesion. But what do these terms mean?
Coupling & Cohesion
Dependencies between different modules represent coupling. Parts of one module can influence another. If you make a change to the code in one module, depending on the coupling, you’ll have to change other modules as well.
On the other hand, cohesion describes how the units inside of a given module belong to one another.
So, the goal is to group as much related code as possible. That way, your code is maintainable over time and scales as your app grows.
With this in mind, think about how you organize code when implementing the Android UI. Note that you have two modules that depend on each other.
Why did you define these modules like this? Well, each of the modules is written in another language, and the differences between them cause the framework to require this kind of design.
Your ViewModel
and layout can be closely related and, therefore, coupled but you don’t have a choice of drawing the line of separation, because you write them in different languages, with different semantical properties.
In other words, these units should be cohesive, but they can’t be because of the language difference. However, they rely on each other in order to work, but the dependency is implicit.
Again, this is because of the language difference and the fact that you can’t directly communicate to the XML file. You have to inflate it into a Kotlin or Java object, and then communicate with that.
For this reason, as your ViewModel
and your layout grow, they become very difficult to maintain.
Imperative thinking
Whenever the user performs a specific action in your UI, you have to capture that event, update the View state and the UI to represent the newly given state. You do this over and over throughout the lifecycle of your app.
If you want something to animate when your state changes, you need to define how your view changes between different states. This programming style is known as imperative programming.
Being imperative means you are commanding or telling the program to do what you want or show the View
s you need. It uses statements in the form of functions to change a program’s state. Imperative programming focuses on describing how a program should operate.
Traditionally, in Android, you use the imperative style to build and manage your UIs. You often create a fully-functional UI and mutate it later using functions, when the state changes.
Imagine a UI for a calendar event. The required properties are a title, an event owner and a link to an online meeting. Other optional properties are a guest list and a room name. If there are more than five guests, the app should collapse the guest list.
With imperative thinking, you might use the following code to render the event card:
fun renderEventCard(event: Event) {
// Handle event title
setTitle(event.title)
// Handle event owner
setOwner(event.owner)
// Handle event call link
setCallLink(event.callLink)
// Handle guest list visibility
if (event.guests.size > 0 && !hasGuestList()) {
addGuestList()
} else if (event.guests.size == 0 && hasGuestList()) {
removeGuestList()
}
// Handle case with more than 5 guests
if (event.guests.size > 5 && !isGuestListCollapsed()) {
collapseGuestList()
} else if (event.guests.size > 0 && isGuestListCollapsed()) {
expandGuestList()
}
// Handle guest count badge
if (event.guests.size <= 50) {
setGuestCountText("$count")
} else {
setGuestCountText("50+")
}
// Handle Event Room
if (event.isRoomSelected && !isRoomAdded()) {
addRoom()
} else if (!event.isRoomSelected && isRoomAdded()) {
removeRoom()
}
}
The function is just a showcase of how many different states you would need to handle for the given case of a calendar event.
After setting the basic properties like the title, owner and call link, you have to handle four different if
statements for various states the card might have — like showing the collapsable guest list, guest count or meeting room.
When building the UI for this use case, you need to think about the following:
- Which UI you’d like to display for any given data.
- How to respond to events.
- How your UI changes over time.
Just by the number of if
checks, there are sixteen different states for your calendar card.
This seemed like a simple use case, but it’s actually very complex, and lots of bugs can creep in as you develop it. Imagine a more complex example, where you also need to handle the animations between states.
Handling how the UI changes over time is the hardest part of building a UI. But if you can generalize the behavior, you can try to simplify it! :]
Inheritance
One way to make your life easier when it comes to updating your UI is to extract some parts of your UI to custom view classes.
If you have components that you often reuse throughout your app, just create a custom view and keep all that code in one place.
You already saw that making a custom view is hard, but it’s useful in situations like this. There’s a well-known principle that describes how to organize your code that way — composition over inheritance.
This is a principle in object-oriented programming (OOP) that says that classes should achieve polymorphic behavior and code reuse by their composition — that is, by containing instances of other classes that implement the desired functionality — rather than inheriting from a base or parent class.
In the current Android UI API, inheritance plays a huge part — and that causes problems. For example, Button
is one of the most-used widgets in every Android app. When you look at Button
‘s class hierarchy, you’ll see something like this:
It seems to make sense, as Button
s display text. But the issue is that TextView
s can do a lot of things. For instance, you can make a button selectable, if you want.
Now, what about ImageButton
? Can you guess what class ImageButton
extends? Well, take a look at its class hierarchy:
As you see, you now have two buttons that share the same logic except that one uses an image, while the other uses text for its content. Yet, they extend different classes.
Now, imagine that in some crazy world you need a button with an image and text. Which class should you extend? That causes a problem.
Inheritance not only introduces a lot of unnecessary logic, but it also limits the classes you can inherit from, as in Kotlin and Java, you can only inherit from one parent, there is no multiple inheritance.
Data flow
There’s one more important thing to look at in the current Android UI tookit, which has to do with state and the data flow between the UI and the business logic. When talking about state, it’s important to consider three main questions:
- What is the source of truth?
- Who owns the state?
- Who updates the state?
It’s not simple to answer these questions in Android development, which is why you have so many different architectural patterns.
You have Model-View-Controller (MVC), Model-View-ViewModel (MVVM), Model-View-Presenter (MVP), Model-View-Intent (MVI) and probably many more Model-View-Somethings. These patterns exist to help define and reason about your app’s data flow and state management.
Take the Spinner
component, for example.
Spinner
offers a listener called onSelectedItemChanged
that tells you when a user changed the value — but it happens after the value changes. It’s hard to build your UI to be a representation of your model if your UI also owns and manages the state.
In this example, Spinner
will update its state and notify you about the state change. Your model will update the state and, if you have the logic to update the Spinner
when your state changes, you’ll update the Spinner
once more for that change.
But you cannot know if you are the one changing the Spinner
state, or if the event came from the user.
It seems you’ve been reading about a lot of complexity and issues the current UI toolkit provides, so if some of those hit you hard, you’ll be glad to hear that there is hope — and it’s waiting just around the corner! :]
Introduction to Jetpack Compose
In the previous sections, you went through some important concepts and issues regarding the original Android UI toolkit. Now, it’s time to say hello to Jetpack Compose!
Jetpack Compose is Android’s modern toolkit for building native UI. When you learned about how difficult building a UI in Android with the original Android UI toolkit is, you started with the basic, familiar building blocks, View
and ViewGroup
. So it makes sense to kick off this introduction with the basic building blocks of Jetpack Compose — composable functions.
Composables
Like all great tutorials, this one will start with the “Hello World!” example! :]
@Composable
fun GreetingWorld() {
}
You can break this code into two parts: First, it’s a function and second, that function has an annotation called @Composable
.
That’s pretty much all you need to create a new widget. Ready for the big surprise? In Compose’s world, you call these widgets composables.
You’ll notice that you don’t have to extend any class (looking at you, View
) or overwrite constructors or other functions. All you need to care about is that you write a function and use this new fancy annotation.
Get ready because you’ll see that one a lot!
In this example, you want to show a message to the user that says “Hello World!”. To do that, you can do the following:
@Composable
fun GreetingWorld() {
Text(text = "Hello world!")
}
In Compose, calling a function that displays something on the screen is known as emitting the UI. So, to emit your message, you need to call a Text
function.
Text
is also a composable function and it’s one of the default composable functions that make up Jetpack Compose. One thing to notice is that composable functions can only be invoked from other composable functions — if you try to remove @Composable
, you’ll get an error stopping you from using Text()
.
But what if you want to pass in a specific message or name, or any kind of parameter to the composable? Well, you can do something like this:
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}
Since composables are functions, you can pass data to them as function parameters. In your example, you add a property name
to Greeting()
and use that data when invoking Text()
.
The simplest way to think about composables is to understand they’re functions that take your data and transform it into your UI. Another way to put it — in Compose, the UI is a function of the data (state).
In the past few years you’ve probably heard how functional programming, and having pure functions is all the rage. By using Compose you can be a part of the cool group of kids! :]
All jokes aside, this functional paradigm makes it simpler to write and refactor the code. It also makes it easier to visualize it.
Displaying composables
When it comes to displaying your composables, you still use activities or fragments as a starting point. To display the Greeting
composable you just saw, you’d do the following:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Greeting("World")
}
}
}
@Composable
fun Greeting(name: String) {
Text (text = "Hello $name!")
}
You connect composables with your activities using a content block. setContent()
defines the Activity
’s layout. Instead of defining the layout content with an XML file, you just call composable functions.
The magic behind Jetpack Compose transforms these composable functions into the app’s UI elements. Chapter 2, “Learning Jetpack Compose Fundamentals”, will explore this in more detail!
Using Kotlin
If you haven’t noticed so far, Jetpack Compose allows you to write your UI using Kotlin, the amazing language that is so much better than rusty old XML. Not just that — Jetpack Compose is built entirely in Kotlin!
Let’s see how using Kotlin features and programming practices that follow OOP can solve the difficulties you read about earlier.
Separation of concerns
You’ve seen how having different languages to build your business logic and your UI leads to forced separation of concerns. With Jetpack Compose, that line disappears.
By using Kotlin to write your UI, you take responsibility for drawing the line of separation. And you can do whatever makes the most sense in your situation. You don’t have to conform to the limitations of the operating system.
That means that many of the implicit dependencies that you had between your layout and business logic now become explicit.
Having your UI and business logic written in the same language allows you to refactor those dependencies to reduce the coupling and increase cohesion in your code.
A framework shouldn’t separate your concerns for you. This doesn’t mean that you should mix your logic and UI. But by writing everything in Kotlin, you can apply all the good practices to define where you want to draw the line between the two.
Declarative thinking
You’ve read about how the design of the original Android UI toolkit requires you to write imperative code. Whatever architecture you use, you’ll find yourself writing the code that describes how your UI changes over time.
In Jetpack Compose, you’ll have to shift how you think about UI in terms of declarative programming.
Declarative programming is a programming paradigm where you don’t focus on describing how a program should operate, but what the program should accomplish — e.g. it should show a hidden Button
, rather than it should hide a Button
.
Remember that example with the event card? You can write that same logic declaratively using Jetpack Compose.
@Composable
fun EventCard(event: Event) {
Title(event.title)
Owner(event.owner)
Call(event.callLink)
if (event.guests.size > 0) {
Guests(collapsed = event.guests.size > 5) {
if (event.guests.size > 50) {
Badge(text="50+")
} else {
Badge(text="$count")
}
}
}
if (event.isRoomSelected) {
Room(event.room)
}
}
Note how you described what should happen without saying anything about how it should happen. The app will add the guest list and the room name only if they exist. The list collapses when there are more than five guests.
Do you remember those concerns about the UI state? You no longer need to worry about how your UI changes over time. By using this declarative approach, you describe how the UI should look, while the framework controls how you get from one state to the other.
Additionally, the composable function is a function definition, but it describes all possible states of your UI in one place.
Composition
In the example with Button
s and ImageButton
s, you saw how inheritance can lead to specific problems. Jetpack Compose allows you to address those problems by favoring composition over inheritance. By examining the same example, you’ll see how to approach that problem.
To make a button widget, you do something like this:
@Composable
fun TextButton(text: String) {
Box(modifier = Modifier.clickable(onClick = { ... })) {
Text(text = text)
}
}
Note how, in this example, you have a Box
, which is a composable that lets you stack multiple composables. It’s also using Modifier.clickable()
, to make the composable clickable. For now, you don’t need to know about modifiers.
You’ll learn about the Box
component in later chapters. And in Chapter 6, “Using Compose Modifiers” you’ll learn about modifiers in greater detail. For now, just know that Box()
allows you to wrap your Text()
and you use a Modifier
to make it clickable.
Now, what if you need a button with an image, as in the previous example? You do something like this:
@Composable
fun ImageButton() {
Box(modifier = Modifier.clickable(onClick = { ... })) {
Icon(painterResource(id = R.drawable.vector), contentDescription = "")
}
}
Here, you use Icon()
instead of Text()
. The next chapter will cover this in more detail. For now, it’s enough to say that this composable allows you to display vectors or static images.
To wrap up the example, if you need a button with text and an image, you can now do this:
@Composable
fun TextImageButton(text: String) {
Box(modifier = Modifier.clickable(onClick = { ... })) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(painterResource(id = R.drawable.vector), contentDescription = "")
Text(text = text)
}
}
}
Here, you combined the Icon
and the Text
composables in one Row
. A Row
is similar to a horizontal LinearLayout
, where items will be positioned one next to the other, horizontally. By doing so, you successfully created a button with an image and text. So easy! :]
And that’s it. With Compose, there’s no need to add anything, you simply add the composables you need. You build your widgets from other widgets that you’ve already created or that Jetpack Compose offers you.
Encapsulation
In the Data Flow section, you saw how the data flows between the UI and your business logic. You saw that in the old Android UI toolkit, views also manage state and that they expose callbacks that you use to capture the change in that state.
Compose is designed in a way that your UI is a representation of your data. That means that your UI components — in this case, composables — aren’t responsible for managing state. They represent your state.
Kotlin allows you to pass down data in the form of function parameters, but you can also pass the callbacks to propagate the events up. This is how you implement the public APIs of your composable functions.
Imagine you want a list of posts that the user can click. You could create a Post
composable like this:
@Composable
fun Post(post: PostData, onClickAction: () -> Unit) {
Box(modifier = Modifier.clickable(onClick = onClickAction)) {
Row {
Icon(bitmap = post.image, contentDescription = "")
Text(text = post.title)
}
}
}
You pass the PostData
down to the function to render a specific Post
and you also pass a callback to receive the click event when the user clicks the Post
.
You should strive to have top-down data flow, which means that data should flow down to your UI (your composables), and events should flow from your UI to your ViewModel
s.
Recomposition
Recomposition is one of the most important concepts in Jetpack Compose. To put it simply, recomposition allows any composable function to be re-invoked at any time.
This is very useful when updating your UI with new state. Whenever state changes, Compose will re-invoke all the composables that depend on that state and update your UI.
Chapter 2, “Learning Jetpack Compose Fundamentals” will go into more detail about the recomposition step. For now, you should just be aware that this concept means you don’t have to manually update your UI when the state changes, as you had to do with the old Android UI toolkit.
Now, imagine you have a list of posts like this:
@Composable
fun Posts(livePosts: LiveData<List<Post>>) {
val posts by livePosts.observeAsState(initial = emptyList())
if (posts.isNotEmpty()) {
PostList(posts)
} else {
MessageForEmptyPosts()
}
}
If the number of posts isn’t zero, you show a list of posts. If the number of posts is zero, you show a message.
Here, you can see how you can use LiveData
and observe it as a state. You’ll learn more about this in Chapter 7, “Managing State in Compose”.
The idea behind this is that you observe the Posts
’ state. Whenever the livePosts
data changes, Compose re-invokes Posts()
and re-evaluates the logic inside.
If there are no posts, Compose calls MessageForEmptyPosts()
and your app shows a message for the user. If there are posts, Compose calls PostList(posts)
and your app shows the list of posts.
Jetpack Compose’s tech stack
By now, you should have a better idea of how Compose tries to solve the problems of the old Android UI toolkit. You’ve seen some examples of what’s possible with Jetpack Compose, but how is it all wired under the hood?
Two parts categorize the different components that make up Jetpack Compose: the development host and the device host.
Development host
The development host contains all the tools that help you write your code.
At the bottom, you have a Kotlin compiler. Jetpack Compose is written in Kotlin and uses a lot of Kotlin features, which is what makes it so flexible and easy to use. You’ve seen how Compose uses trailing lambdas to make the code more readable and intuitive.
On top of that, you have a Compose Compiler Plugin. Even though you use @Composable
as an annotation, Compose doesn’t use the annotation processor. This plugin works at the type system level and also at the code generation level to change the types of your composable functions.
If you’re not familiar with annotation processors, or APTs, they are a special system within the build process that analyze specific annotations and generate code based on them.
This is a great thing because you can use the generated code instead of writing it yourself, but sometimes it’s terrible as it greatly increases the build time for your project. But because Compose doesn’t use APTs, it doesn’t slow down your builds!
On top of that, you have Android Studio, which includes Compose-specific tools, simplifying the work you do with Compose.
Device host
The second part of this tech stack is your device; that is, the environment that runs your Compose code.
At the bottom, there’s a Compose Runtime. At its core, the Compose logic doesn’t know anything about Android or UIs. It only knows how to work with tree structures to emit specific items. That makes it even more interesting because you could use Compose to emit things other than UIs.
On top of that lies Compose UI Core. It handles input management, measurement, drawing, layout etc.
These two layers support the widgets that the next layer provides — Compose UI foundation. It contains basic building blocks like Text
, Row
, Column
and default interactions.
Finally, there’s Compose UI Material, an implementation of the Material Design system. It provides Material components out of the box, making it easy to use Material Design in your app.
Key points
- View.java’s size makes the old Android UI toolkit hard to maintain and scale.
- Creating custom views is hard and requires too much code.
- Unlike imperative programming, declarative programming simplifies code and makes it easier to understand.
- In the old Android UI toolkit, it’s not clear what the source of truth is, who owns it and who updates it.
- In Jetpack Compose, you use composables to build your UI.
- Composables are just functions annotated with
@Composable
. - Jetpack Compose is written in Kotlin and allows you to use all the Kotlin features.
- In Jetpack Compose, your UI is a function of data.
- You use
setContent()
as the entry point to display your composables. - In Compose you control where to draw the line of separation of concerns between your business logic and UI.
- Jetpack Compose favors composition over inheritance.
- In Jetpack Compose, you use function parameters to pass down the data and callbacks to propagate events up.
- Jetpack Compose uses recomposition to re-invoke composables when the state changes.
- Jetpack Compose doesn’t use the annotation processor, but rather a Compose Compiler Plugin that changes the type of composable functions.
Where to go from here?
That was a really brief comparison of the original Android UI toolkit and Jetpack Compose! :]
By now, you should have a feeling of the potential of Jetpack Compose.
Keep the excitement up for the following chapters, where you’ll get your hands dirty while learning about some of the existing composables that you can use to build your UI.
In the first section of the book, you’ll cover the fundamentals, learn how to use layout and how to create lists — one of the most important UI components! See you there!