Chapters

Hide chapters

Jetpack Compose by Tutorials

Second Edition · Android 13 · Kotlin 1.7 · Android Studio Dolphin

Section VI: Appendices

Section 6: 1 chapter
Show chapters Hide chapters

1. Developing UI in Android
Written by Prateek Prasad

The user interface (UI) is the embodiment of your mobile app. You could say 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 one of the most common reason products succeed or fail.

In this chapter, you’ll learn the design concepts behind the existing Android UI toolkit. You’ll review the basics of showing the layout on screen, how to make custom views, and the technical principles behind it. You’ll learn the reasons behind these concepts, their drawbacks, and their influence on 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, how it improves upon them, and why it’s the next evolutionary step in Android development.

Unwrapping the Android UI Toolkit

In Android, you build your UI as a tree-based hierarchy of layouts and widgets. In code, layouts are represented by ViewGroup objects. They are containers controlling 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.

View Hierarchy
View Hierarchy

As you can see in the image, you define each screen in Android as a tree of ViewGroup and View objects. ViewGroups can contain other ViewGroups and Views. If you’re familiar with computer science structures, you’ll recognize ViewGroups are like nodes of the tree structure, where each View is a leaf.

The most important thing to notice here is your View objects are responsible for the look of your UI. So it makes sense to begin by looking at how you implement and use the View class.

View

As mentioned before, a View in the Android UI toolkit, 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.

Users Interact with the UI
Users Interact with the UI

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, Views also support this type of interaction and events. Specialized Views often expose a specific set of event 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.

Lines of Code in View.java
Lines of Code in View.java

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 the current Android UI toolkit scales poorly and is increasingly harder to maintain.

Imagine yourself fixing 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 building 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?

Think again! If you want to build even the simplest of custom Views, you 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 to do is create a class extending from View. Writing custom views to solve a particular problem is hard. You need to:

  1. Override the View constructors. Yes, there are multiple, each with its own use case!
  2. To inflate the specific layout, you have to define it as an XML resource.
  3. To customize your View from XML, you have to create special XML attributes and add them to the attrs.xml file.
  4. To modify your custom widget, you have to add the necessary properties and their respective getters and setters to the class.
  5. You have to think about styles and how your View behaves in different display modes, such as light and dark theme.
  6. If you need custom measurements or layouts, you have to override the specific callbacks.
  7. Do you need to handle touch events? Then you need extra code to add touch & gesture support!

There are a lot of things to think about when writing custom views. And as a developer, you want a clean and easy API you can easily expand with your custom implementation. Unfortunately, the current Android UI toolkit is anything but easy.

ViewGroup

After implementing your custom view, you need to add it to your UI. But before you do, you have to choose the correct ViewGroup for your container. 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 have to choose which ViewGroup to use as your root view.

There are many different types of ViewGroups in Android. Some common implementations are LinearLayout, RelativeLayout and FrameLayout. Each of these expose 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, it lets you stack widgets, vertically on top of one another. It is usually used to host a single widget or a Fragment.

Nested ViewGroups
Nested ViewGroups

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 best matching your use case.

That leads to a lot of nested ViewGroups, making 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 the creation of 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 layouts. Each constraint describes if a View is constrained to the start, end, top or bottom of another View.

This doesn’t quite solve the problem of nested layouts. There are times when you can get better performance by combining simpler ViewGroups rather than using ConstraintLayout.

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? :]

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

Imagine you’ve successfully created your layout. You picked the right Views. You created a custom View to solve a specific problem. You used the correct ViewGroups to organize your Views. Now, you want to display your beautiful layout.

But there are still many more steps you need to take to achieve this behavior!

If you’re experienced with the Android UI toolkit, you know usually your UI is defined in XML files. Android provides an XML schema for including ViewGroup and View classes. 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”.

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:

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 to display UI elements in Android.

Fragments, on the other hand, represent a piece of behavior or a portion of the UI within an Activity. You can combine multiple Fragments 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, onCreateView() provides a LayoutInflater — a 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 called layout inflation, as layouts are similar to balloons — you need to inflate them to take shape and do some work! :]

Non-scalable Layout System
Non-scalable Layout System

Imagine that you you need a screen with an Activity and a Fragment inside it. To create the screen, you 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 numerous features each with its own XML layout, attributes, styles, Kotlin or Java code and much more.

The volume of files makes it difficult 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. If you want to see your UI in action, you have to connect it to your business logic.

It’s good practice to 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 affecting 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.

Coupling
Coupling

On the other hand, cohesion describes how the units inside of a given module are related to one another.

Cohesion
Cohesion

So, the goal is to group as much related code as possible. This makes your code maintainable over time and scalable as your app grows. More importantly, the impact of any change you make is isolated to its own module.

With this in mind, think about how you usually organize code when implementing the Android UI. Note that you have two modules that depend on each other.

Modules when Implementing Android UI
Modules when Implementing Android UI

Why did you define these modules like this? Well, each of these modules are written in distinctly different languages, and the differences between them cause the framework to force 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. They rely on each other in order to work, but the dependency is implicit.

This is because of the language difference and the fact you can’t directly communicate to the XML file. You have to inflate it into a Kotlin or Java object, and communicate with that.

For this reason, as your ViewModel and your layout grow, they become very difficult to maintain.

Forced Line of Separation of Concerns
Forced Line of Separation of Concerns

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 Views you need. It uses statements in the form of functions to change a program’s state. Imperative programming focuses on describing the steps for how a program should operate.

Traditionally, in Android, you use the imperative style to build and manage your UIs. You define the layout in XML and mutate it later using functions in Java/Kotlin.

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.

Calendar Event
Calendar Event

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 collapsible 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 parts of your UI to custom view classes.

If you have components you often reuse throughout your app, just create a custom view and keep all that code in one place.

You already saw making a custom view is hard, but it’s useful in situations like this. There’s a well-known principle describing 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.

Inheritance vs Composition
Inheritance vs Composition

In the current Android UI toolkit, inheritance plays a huge role — 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:

Button's Class Hierarchy
Button's Class Hierarchy

It seems to make sense, as Buttons display text. The issue is TextViews do a lot of things. For instance, you can make a button selectable.

Selectable Button
Selectable Button

What about ImageButton? Can you guess what class ImageButton extends? Well, take a look at its class hierarchy:

ImageButton's Class Hierarchy
ImageButton's Class Hierarchy

As you see, you have two buttons sharing the same logic except one uses an image, while the other uses text for its content. Yet, they extend different classes.

Now, imagine you need a button with an image and text. Which class should you extend? The answer isn’t simple.

Inheritance not only introduces a lot of unnecessary logic, it limits the classes you can inherit from. In Kotlin and Java, you can only inherit from one parent, there is no multiple inheritance.

Data Flow

There’s one more thing to look at in the current Android UI toolkit, 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 many more. These patterns help define and reason about your app’s data flow and state management.

Take the Spinner component, for example.

Spinner and State Management
Spinner and State Management

Spinner offers a listener called onSelectedItemChanged telling 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 their internal 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 reliably know if you are the one changing the Spinner state, or if the event came from the user. The onus to keep the Spinner‘s internal state and the model’s state in sync is on you, which introduces a lot of unnecessary complexity and room for errors.

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! :]

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 override 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 making up Jetpack Compose. One thing to notice is 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 another kind of parameter to the composable? Well, you can do 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).

UI as a Function of Data
UI as a Function of Data

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! :]

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 composables, you still use activities or fragments as a starting point. To display the Greeting composable you just saw, you 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 is how it transforms these composable functions into the app’s UI elements. Chapter 2, “Learning Jetpack Compose Fundamentals”, will explore this in more detail!

Using Kotlin

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.

Control of the Line of Separation of Concerns
Control of the Line of Separation of Concerns

By using Kotlin to write your UI, you take responsibility for drawing the line of separation. You can do whatever makes the most sense in your situation. You don’t have to conform to the forced limitations of the operating system.

Explicit Dependencies
Explicit Dependencies

That means many of the implicit dependencies you had between your layout and business logic now become explicit.

Refactoring Explicit Dependencies
Refactoring Explicit Dependencies

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. For example, you need to describe how your UI should show a hidden Button, rather than how 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.

This representation is purely based on the state of the event model. Making it much easier to understand what exactly is going on.

Remember those concerns about syncing your UI and your model state? You no longer need to worry 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 Buttons and ImageButtons, 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 letting you stack multiple composables, vertically on top of one another. It’s also using Modifier.clickable(), to make the composable clickable. For now, you don’t need to worry 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 covers this in more detail. For now, it’s enough to say 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 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 (i.e composed) 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! :]

Since Compose is based on functions and there is no inheritance involved by design, there is no need to override functionality you do not need.

And that’s it. With Compose, there’s no need to do anything extra, 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 in the old Android UI toolkit, views also manage their own internal state and they expose callbacks that you use to capture the change in their internal state.

Compose is designed in a way where your UI is a representation of your data. Meaning your UI components — in this case, composables — aren’t responsible for managing state. They represent your state.

Data Flow
Data Flow

Kotlin allows you to pass down data in the form of function parameters, but you can also pass the callbacks to propagate the events to mutate the state 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 Unidirectional data flow, which means data should flow down to your UI (your composables), and events should flow from your UI to your ViewModels.

Recomposition

Recomposition is one of the most important concepts in Jetpack Compose. It’s the mechanism used by compose to update the UI based on state changes. Recomposition allows any composable function to be re-invoked at any time to re-render the component based on new data.

Understanding recomposition is 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, 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.

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 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 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.

All of this happens automagically!

Jetpack Compose’s Tech Stack

By now, you have a better idea of how Compose tries to solve the problems of the old Android UI toolkit. You’ve seen 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.

Jetpack Compose Tech Stack
Jetpack Compose Tech Stack

Development Host

The development host contains all the tools to 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 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 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. This is called Unidirectional data flow
  • 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 sense 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 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!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.