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

2. Learning Jetpack Compose Fundamentals
Written by Prateek Prasad

In this chapter, you’ll cover the basics of Jetpack Compose. You’ll learn how to write composable functions, the building blocks used to create beautiful UI with Jetpack Compose. You’ll see how to implement the most common composable functions such as text, image or button elements. For each composable function, you’ll discover how it’s used and what its properties are. Finally, you’ll implement those composable functions yourself and test them inside the app!

Before you start writing code, however, you need to know how an element shown on the screen becomes a composable function.

Composable Functions

In the first chapter, you learned how using XML to build UI differs from using Jetpack Compose. The biggest issues with the former approach are:

  • The UI isn’t scalable.
  • It’s hard to make custom views.
  • State ownership is often scattered between multiple owners.

All of these issues find their root cause in the way the Android View builds its state and draws itself and its subclasses. To avoid those issues, you need to start fresh and use a different basic building block. In Jetpack Compose, this building block is called a composable function.

To make a composable function, you do this:

@Composable
fun MyComposableFunction() {
  // TODO
}

You first annotate a function with @Composable — a special annotation class. Any function annotated this way is also called a composable function, as you can compose it within other composable functions.

Annotation classes simplify the code by attaching metadata to it. Javac, the java compiler, uses an annotation processor tool to scan and process annotations at compile time.

This creates new source files with the added metadata. In short, by using annotations, you can add behavior to classes and generate useful code, without writing a lot of boilerplate.

This specific annotation changes the type of that function or expression to a Composable, meaning that :

  • Only other composable functions can call it
  • The composable can only be invoked from a compose scope

Much like coroutines.

The source code for the Composable annotation class looks like this:

@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
@Target(
   AnnotationTarget.FUNCTION,
   AnnotationTarget.TYPE,
   AnnotationTarget.TYPE_PARAMETER,
   AnnotationTarget.PROPERTY_GETTER
)
annotation class Composable

You can see the Composable annotation class has three annotations of its own:

  1. @MustBeDocumented: Indicates that the annotation is a part of the public API and should be included in the generated documentation.
  2. @Retention: Tells the compiler how long the annotation should live. By using AnnotationRetention.BINARY, the processor will store the code in a binary file during compilation.
  3. @Target: Describes the contexts where the type applies. @Composable can be applied to types, parameters, functions and properties.

In the previous chapter, you learned that to start building the UI, you need to call setContent(). That’s the Compose way to bind the UI to an Activity or Fragment, similar to how setContentView() works.

But it doesn’t work with Views or XML resources, instead it works with composable functions!

Setting the Content

The signature for setContent() looks like this:

fun ComponentActivity.setContent(
   parent: CompositionContext? = null,
   content: @Composable () -> Unit
) { ... }

You can see setContent() is an extension function of ComponentActivity. Extension functions add additional functionality to a class without changing its source code. That means you can use setContent() on any ComponentActivity or its subclasses, like AppCompatActivity.

Calling setContent() sets the given composable function named content as the root view, to which you can add any number of elements. You’ll call the rest of your composable functions from within this container.

Notice how content is also annotated with @Composable. Because of the aforementioned @Target, you can apply it to function parameters, as well.

This specific use case marks the lambda function you pass in as a composable function, allowing you to call other composable functions and access things like resources and the context of Jetpack Compose.

Another parameter inside setContent() is the CompositionContext, which is a reference to the parent composition. CompositionContext is used to coordinate scheduling of composition updates in a composition tree. It ensures that invalidations and data flow logically through the parent and child composition.

The parent of the root composition is a Recomposer which determines the thread where recomposition happens — one of the most important features of Jetpack Compose.

In simple terms, recomposition is an event that asks the app to re-draw the current UI with new values. Recomposition happens every time a value such as state changes.

You’ll learn more about managing states and how recomposition works in Chapter 7, “Managing State in Compose”.

Now you’ve gone over the basics of Jetpack Compose, you can dive into composable functions! :]

Basic Composable Functions

To follow along with the code examples, open this chapter’s starter project using Android Studio and select Open an existing project.

Next, navigate to 02-learning-jetpack-compose-fundamentals/projects and select the starter folder as the project root. Once the project opens, let it build and sync and you’ll be ready to go!

The starter project consists of three packages and MainActivity.kt.

Project Packages
Project Packages

Here’s what you should know about the contents:

  • app: Has only one composable function, which acts as a root layout in your app. You won’t need to change it since it only contains the app navigation, which is already set up for you!
  • router: Has two helper classes to handle navigation between screens and the Back button. You won’t need to change anything here, either.
  • screens: Consists of multiple composable functions for different screens. You’ll implement these in this chapter, except for NavigationScreen.kt, which contains a layout for navigation that’s already made for you.
  • MainActivity.kt: Contains the setContent() call, setting the first composable function and acting as a root UI component.

Once you’re familiar with the file organization, build and run the app. You’ll see a screen with basic navigation, as shown below.

Navigation Screen
Navigation Screen

The screen contains five buttons, each leading to an empty screen when you click on it. By pressing Back, you return to the main screen.

Your goal is to implement a composable function for each of the empty screens. So get to it! :]

Text

When you think about the UI, one of the first things that come to mind is a basic text element, or TextView. In Jetpack Compose, the composable function most similar to a TextView is called Text. Let’s see it in action.

Open TextScreen.kt and you’ll see two composable functions: TextScreen() and MyText():

@Composable
fun TextScreen() {
  Column(
    modifier = Modifier.fillMaxSize(), // 1
    horizontalAlignment = Alignment.CenterHorizontally, // 2
    verticalArrangement = Arrangement.Center // 3
  ) {
    MyText()
  }

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}

@Composable
fun MyText() {
  //TODO add your code here
}

TextScreen is already complete. It’s a composable function using a Column component to list out items in a vertical order. In that sense, a Column is just like a vertical LinearLayout!

It also uses modifiers and two Column properties to style the Column, then align it and its children. Here’s more about the properties you’re using:

  1. By using modifiers, you can style each Compose element in multiple different ways. You can change its alignment, size, background, shape and much more. In this case, by using Modifier.fillMaxSize(), you’re telling the Column to match its parent’s width and height.
  2. By using horizontalAlignment, you’re telling the Column to center its children horizontally.
  3. Using verticalArrangement, you tell the Column to vertically centre its children.

You’ll learn more about modifiers in Chapter 6, “Using Compose Modifiers”! Right now, what’s important is how to add text elements to your composable functions. Also ignore the BackButtonHandler(), as it’s a special composable built to handle back clicks, and you don’t need to change it!

Now, change MyText()’s code to the following:

@Composable
fun MyText() {
 Text(text = )
}

You’ll see a variety of choices to import the Text, but make sure to pick the one that comes from androidx.compose.material. You’ll also get a prompt to provide a text to display. Add the following code as the text parameter:

stringResource(id = R.string.jetpack_compose)

Compose has an easy-to-use way to import strings, drawables, and other resources into your UI elements. Normally, to get a string from resources, you call getString() on a given Context. Since you’re working with composable functions, you need a composable function that allows you to do that.

Fortunately, there are many composable functions allowing you to retrieve different types of resources. In this case, you use stringResource(), which takes the ID of a string resource you want to load.

Build and run the app. Then on the main screen, click the Text button. You should see the following screen:

Non-Styled Text
Non-Styled Text

Awesome! There’s now a simple text in the middle of the screen that reads Jetpack Compose. :]

Now you’ve implemented the basic Text(), it’s best to see what other functionality Text() provides. Take a moment to check out what Text() has to offer by inspecting the source code:

@Composable
fun Text(
   text: String,
   modifier: Modifier = Modifier,
   color: Color = Color.Unspecified,
   fontSize: TextUnit = TextUnit.Unspecified,
   fontStyle: FontStyle? = null,
   fontWeight: FontWeight? = null,
   fontFamily: FontFamily? = null,
   letterSpacing: TextUnit = TextUnit.Unspecified,
   textDecoration: TextDecoration? = null,
   textAlign: TextAlign? = null,
   lineHeight: TextUnit = TextUnit.Unspecified,
   overflow: TextOverflow = TextOverflow.Clip,
   softWrap: Boolean = true,
   maxLines: Int = Int.MAX_VALUE,
   onTextLayout: (TextLayoutResult) -> Unit = {},
   style: TextStyle = LocalTextStyle.current
)

It offers a wide range of parameters for different style treatments. The first, text, lets you set the text to display and is the only required parameter.

The second, modifier, is more complex and offers many different features. In the previous example, you saw how Column() used modifiers to fill the parent size. Modifiers allow you to customize the look and feel of your composables. You’ll learn more about modifiers in Chapter 6, “Using Compose Modifiers”.

For now, take a moment to explore some of the parameters the Text() element exposes. Below is a short list of the most common ones:

  • color: Lets you set the text color.
  • fontSize: Changes the font size. You measure it in scalable pixels (sp).
  • fontStyle: Lets you choose between normal and italic font.
  • fontWeight: Sets the weight of the text to Bold, Black, Thin and similar types.
  • textAlign: Sets the horizontal alignment of the text.
  • overflow: Determines how the app handles overflow, using either Clip or Ellipsis.
  • maxLines: Sets the maximum number of lines.
  • style: Lets you build a specific style and reuse it, rather than explicitly setting all the other parameters. The current app theme defines the default style, making it easier to support different themes.

There are many more parameters, but these are the most important and commonly-used ones.

If you want to know more about Text() , use Command-Click on Mac or Control-Click on Windows or Linux to click on the Text function call, and preview the source code and documentation.

Now you’ve displayed text in your UI, it’s time to style it to make it look nicer! :]

Styling Your Text

In this section you’ll display the text in italics with bold weight. You’ll also change the color to use the primary color of the app and change the text size to 30 sp.

Change MyText()’s code to the following:

@Composable
fun MyText() {
 Text(text = stringResource(id = R.string.jetpack_compose),
     fontStyle = FontStyle.Italic, // 1
     color = colorResource(id = R.color.colorPrimary), // 2
     fontSize = 30.sp, // 3
     fontWeight = FontWeight.Bold // 4
 )
}

There are a few things happening here:

  1. Using fontStyle, you make the text italicized by using FontStyle.Italic.
  2. Passing in a color, you change the color of the text. Also notice how colorResource() lets you easily fetch a color from your resources.
  3. The fontSize parameter lets you pass in the size in scalable pixels. Notice the .sp property call. Compose has a way to transform Integer values into dp and sp by calling respective properties! These are extension properties, so make sure to add the . operator.
  4. Finally, fontWeight makes the text bold.

Now build and run the project and open the Text screen to see the new version of your styled text:

Styled Text
Styled Text

You’ve applied all the styles and the text looks much nicer! Feel free to experiment with other parameters and change the text to your own liking.

Upto now, you’ve had to build and run your app every time you made a change in the Text before you could see the result. This makes building complex UI a tedious process because running and building the app for each change takes time. You’ll now learn how to avoid that and make your life easier!

Previewing Changes

When you work with XML, there’s an option to split the screen so you can see both the code and a preview of your UI. You’ll be happy to know Compose offers a similar option!

To use it, you need to annotate your composable function with @Preview, like so:

@Composable
@Preview
fun MyText() {
  Text(...)
}

This allows the Compose compiler to analyze the composable function and generate a preview of it within Android Studio. Now, select the Split option on the top-right side of Android Studio. You’ll see a preview like this:

Preview
Preview

You can also click the small icon above the preview to enter interactive mode. This lets you perform actions and see how the state changes. You don’t need that for the current screen, but it helps when building interactive UI components that require user input.

One thing to keep in mind is that if you’re using preview, your functions need to either:

  • Have no parameters
  • Have default arguments for all parameters
  • Provide a @PreviewParameter as well as a special factory that provides the parameters you want to draw on the UI

You just added your very first composable to the screen, awesome job! But showing static text is a bit dull! Next, you’ll see how to implement an input field, so users can write something in the app!

TextField

In the legacy Android UI toolkit, you’d use an EditText to show input fields to the user. The composable counterpart for an EditText is called a TextField.

Open TextFieldScreen.kt and you’ll see two composable functions:

@Composable
fun TextFieldScreen() {
  Column(
      modifier = Modifier.fillMaxSize(),
      horizontalAlignment = Alignment.CenterHorizontally,
      verticalArrangement = Arrangement.Center
  ) {
    MyTextField()
  }

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}

@Composable
fun MyTextField() {
  //TODO add your code here
}

You’ll make your first TextField inside MyTextField() , similar to what you did in the previous example. Change the code in MyTextField() like so:

@Composable
fun MyTextField() {
  val textValue = remember { mutableStateOf("") }

  TextField(
    value = textValue.value,
    onValueChange = {
      textValue.value = it
    },
    label = {}
  )
}

Make sure you import the TextField package from androidx.compose.material and the remember() and mutableStateOf() packages from androidx.compose.runtime.

That seems like a lot of code for a simple input field, but it will make sense in a minute!

A TextField should allow the user to input text, and the entered text must not disappear or change if the TextField recomposes. For your TextField to work properly, you must provide a value that doesn’t change during recomposition — in other words, a state value. Using mutableStateOf(), you wrap an empty String into a state holder, which you’ll use to store and display the text within the input field.

You also wrapped the state into remember(), which is Compose’s way of telling the recomposer that the value should be persisted through recomposition. If you didn’t use remember() here, every time you changed the state, it would be lost and set to the default value — an empty string.

Next, you connected the value of the textValue holder to the TextField, and within the onValueChange callback, you changed the internal value of the state holder.

What’s going to happen now is every time the user taps on a key on their keyboard, the internal state will change. This will trigger recomposition and re-drawing the TextField with new text. That’s all going to happen really fast, and you won’t be able to notice a difference!

Build and run the app to see your changes. Click the TextField button from the navigation and you’ll see a screen like this:

Non-Styled Text Field
Non-Styled Text Field

It’s a screen with an empty TextField. When you click on that text, a keyboard opens and you can write normally, as you’d expect.

Improving the TextField

If you take a closer look at the screen, you’ll see the current TextField is very basic. It’s missing a hint and some expected default styling, like a border.

To add the hint and the border, you’ll use a special type of TextField called OutlinedTextField. But before you do that, explore the signature of the TextField so you know how you can style the component.

Take a look at the TextField signature, and you’ll see something like this:

@Composable
fun TextField(
    value: TextFieldValue,
    onValueChange: (String) -> Unit,
    label: @Composable () -> Unit,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    keyboardActions: KeyboardActions = KeyboardActions(),
    ...
)

Like the Text composable function, TextField has many parameters to change its style. Since some of the parameters are the same, this section will only explain the most important new ones.

  • value: The current text displayed inside the TextField. Note that it’s of type TextFieldValue and not String.
  • onValueChange: A callback that triggers every time the user types something new. The callback provides a new TextFieldValue so you can update the displayed text.
  • label: The label that’s displayed inside the container. When the user focuses on the text, the label will animate above the writing cursor and stay there.
  • keyboardOptions: Sets the keyboard options such as KeyboardType and ImeAction. Some available KeyboardTypes are: Email, Password and Number while important ImeActions are: Go, Search, Previous, Next and Done.
  • keyboardActions: Allows the developer to set callbacks for keyboard ImeActions.

There are many more parameters, but these are some of the core features you’ll use in most apps.

Feel free to explore these parameters and play around with them. When you’re ready, move onto the OutlinedTextField component, a material design inspired input field! :]

Adding an Email Field With OutlinedTextField

Your next step is to create an email input, one of the most common text fields. Replace the code of MyTextField with the following:

@Composable
fun MyTextField() {
  val textValue = remember { mutableStateOf("") }

  val primaryColor = colorResource(id = R.color.colorPrimary)

  OutlinedTextField(
    label = { Text(text = stringResource(id = R.string.email)) },
    colors = TextFieldDefaults.outlinedTextFieldColors(
        focusedBorderColor = primaryColor,
        focusedLabelColor = primaryColor,
        cursorColor = primaryColor
    ),
    keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Email),
    value = textValue.value,
    onValueChange = {
      textValue.value = it
    },
  )
}

An OutlinedTextField is a styled TextField, it uses a special internal function to draw and animate a border around the field and a description text. This composable is most similar to the TextInputLayout XML widget that ships with the material library.

To add a hint, or a label as it’s known in Compose, you use the label property and pass in another composable function. This is the beauty of Compose — whenever you need some functionality, you can use other composable functions to fill that need. In this case, you need to display a text that gives the user a hint about what the input data should be by using a Text().

The second parameter you added is colors. It changes the colors for different parts of the TextField. In this case, you use the primary color from resources to change the border and label colors in focused state and cursor color.

The last change is to change the keyboardType to KeyboardType.Email. To do this you use the KeyboardOptions.Default instance of KeyboardOptions and make a new copy of the object with the desired keyboardType. This will open a keyboard that makes it easier to write email domains when TextField is in focus.

Build and run your app to test your new email input. The TextField screen will look like this:

Styled TextField
Styled TextField

The text field has a border and a hint that reads: Email. Click it to gain focus.

Focused TextField
Focused TextField

The hint animates to the top of the border and your text field comes to life in green. Nice! :]

Next, you’ll learn how to add a button and how to handle click events.

Buttons

With what you’ve learned so far, you know how to read text from a screen and how to display it. The last thing you need to make a basic form is a button.

There are many types of buttons in the Android world, but all of them have one thing in common, they can be clicked. Next, you’ll see how to implement one, and how to handle the click actions!

Open ButtonsScreen.kt and look at the code:

@Composable
fun ExploreButtonsScreen() {
  Column(
    modifier = Modifier.fillMaxSize(),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.Center
  ) {

    MyButton()
    MyRadioGroup()
    MyFloatingActionButton()

    BackButtonHandler {
      JetFundamentalsRouter.navigateTo(Screen.Navigation)
    }
  }
}

@Composable
fun MyButton() {
  //TODO add your code here
}

@Composable
fun MyRadioGroup() {
  //TODO add your code here
}

@Composable
fun MyFloatingActionButton() {
  //TODO add your code here
}

You can see there are four composable functions in the file. ExploreButtonsScreen() centers and displays the main layout. You’ll use the three empty functions to practice working with the different types of buttons.

Building a Login Button

First, you’ll make the basic button you’d expect to see while logging in. Start by adding the following code to MyButton():

@Composable
fun MyButton() {
  Button(
    onClick = {},
    colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.colorPrimary)),
    border = BorderStroke(
      1.dp,
      color = colorResource(id = R.color.colorPrimaryDark)
    )
  ) {
    Text(
      text = stringResource(id = R.string.button_text),
      color = Color.White
    )
  }
}

In the code above, you aren’t performing any actions when the user clicks the button. However, you are using an empty lambda expression as onClick to keep it enabled.

To change the background color of the button, you use the ButtonDefaults instance and call buttonColors method on it with the desired background color as a parameter. This method also allows you to change disabledBackgroundColor, contentColor and disabledContentColor if needed.

You also use a BorderStroke to set the background color and add a border with a width of 1 dp and a dark primary color. Each BorderStroke has to define a color and its width. You can add them to many components, such as buttons, cards and much more.

Finally, you add a Text() as the content of the button, as you learned previously, and set the text color to Color.White. The Color component is another part of the Compose framework that defines commonly used colors like White, Black, Gray and so on.

Now, build and run the app and open the Buttons screen.

Button
Button

This is how a button with a border looks in Jetpack Compose. It’s a simple component following Material Design. You haven’t added any specific actions within the onClick handler, but you get the idea! You can set it up to call any functions you want to execute, any time the user taps the button.

Exploring Button

Now, look at the signature of a Button composable function to see what it can do:

@Composable
fun Button(
    onClick: () -> Unit,
    enabled: Boolean = true,
    elevation: Dp = 2.dp,
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    content: @Composable RowScope.() -> Unit,
    ...
)

Below is a list of the the most important parameters to get a better understanding:

  • onClick: The most common property you’ll use with buttons, this calls a function when the user clicks the button. If you don’t provide onClick, the button will be disabled.
  • enabled: Allows you to control when a button is clickable.
  • elevation: Sets the elevation of a button. The default elevation is 2 dp.
  • shape: Defines the button’s shape and shadow. With MaterialTheme.shapes, you can choose a shape’s size: small, medium or large. Or you can specify a custom shape as well.
  • border: Draws a border around your button.
  • content: A composable function that displays the content inside the button, usually text.

Again, there are many more parameters, but for the sake of simplicity, only the most important ones are listed.

Now that you know what’s possible with Button, you can create as many buttons, along with their borders and background colors, as you need!

Next, you’ll make a radio button or, more specifically, a group of radio buttons.

RadioButton

The composable function you use to make radio buttons is named RadioButton. A radio button is a small, circular button the user can select. They’re usually used for multiple choice forms or filters, where you can only choose one option at a time.

For example, you might have one radio button to opt in to receiving a newsletter and another to opt out, and only one of the two choices can be selected at the same time. This type of component is called a radio group.

At this time, Jetpack Compose doesn’t have an implementation for a radio group so you’ll have to make a custom group yourself! Don’t worry though, you will get a sense of how easy it is to fill some of the API’s gaps yourself.

Change the code in MyRadioGroup to the following:

@Composable
fun MyRadioGroup() {
  val radioButtons = listOf(0, 1, 2) // 1

  val selectedButton = remember { mutableStateOf(radioButtons.first()) } // 2

  Column {
    radioButtons.forEach { index -> // 3
      val isSelected = index == selectedButton.value
      val colors = RadioButtonDefaults.colors( // 4
        selectedColor = colorResource(id = R.color.colorPrimary),
        unselectedColor = colorResource(id = R.color.colorPrimaryDark),
        disabledColor = Color.LightGray
      )

      RadioButton( // 5
        colors = colors,
        selected = isSelected,
        onClick = { selectedButton.value = index } // 6
      )
    }
  }
}

There’s a lot going on in the snippet above so here’s a breakdown to make it easier to understand:

  1. You create a list of three different options with values ranging from 0 to 2. These options are indices representing each radio button.
  2. You create a selectedButton state that remembers which button is selected. It also selects the first button by default.
  3. Using a forEach loop, you add a button to your Column in each iteration of the loop.
  4. You can change the color of a RadioButton using the RadioButtonDefaults.colors(). You pass in a color for each of the different states RadioButton can appear in.
  5. At the end of each loop iteration, you build a RadioButton and set both the onClick handler and its color when it’s selected.
  6. Every time a user taps the button, you’ll change which button is selected in the state. This triggers a recomposition and your UI will update!

Now, build and run your app to try your new creation.

Radio Button
Radio Button

You now see three radio buttons on the screen. The first is selected by default. When you select another radio button, you can see the animation that switches between the buttons.

Exploring RadioButton

To learn more about RadioButton, look at its signature:

@Composable
fun RadioButton(
    selected: Boolean,
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    interactionState: InteractionState = remember { InteractionState() },
    colors: RadioButtonColors = RadioButtonDefaults.colors()
)

There are fewer parameters than usual, here are the most important ones:

  • selected: Toggles the current state of the button between selected and not selected.
  • interactionState: Allows you to define interactions such as drag gestures and touches.
  • colors: The color combination for the RadioButton. Use the RadioButtonDefaults instance to call colors() on it to change the default color for different states. The available colors for different states are selectedColor, unselectedColor and disabledColor.

You’re almost done with this overview of commonly used UI components. There’s one more type of buttons for you to complete — FloatingActionButtons!

FloatingActionButton

Floating action buttons are named that way because they have a higher elevation that places them above all content. They’re used to place the primary action of your app within easy reach for your users.

For your next step, you’ll create a simple floating action button using an icon. Start by changing the code in the MyFloatingActionButton to the following:

@Composable
fun MyFloatingActionButton() {
  FloatingActionButton(
      onClick = {},
      backgroundColor = colorResource(id = R.color.colorPrimary),
      contentColor = Color.White,
      content = {
        Icon(Icons.Filled.Favorite, contentDescription = "Test FAB")
      }
  )
}

Here, you add an empty lambda expression to keep the button enabled. Next, you set the background and content color. Finally, you set the icon by using Icon() and the predefined, filled, Favorite icon and a test contentDescription for accessibility.

The Icons object contains predefined and commonly used icons in the Android world in their vector form. Similar to what the Color object does for colors. You can choose between Filled, Default, and Outlined style treatment for these default icons.

Exploring FloatingActionButton

To learn more about the FloatingActionButton, check out its signature:

@Composable
fun FloatingActionButton(
   onClick: () -> Unit,
   modifier: Modifier = Modifier,
   shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
   backgroundColor: Color = MaterialTheme.colors.secondary,
   contentColor: Color = contentColorFor(backgroundColor),
   elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
   content: @Composable () -> Unit
)

You’re already familiar with most, if not all of the, parameters. The important thing to remember here is a FloatingActionButton has an elevation, is clickable and you add content to it by using another composable function. In most cases, you’ll want to use an Icon() for the content. The signature of an Icon() is very simple:

@Composable
fun Icon(
  imageVector: ImageVector,
  contentDescription: String?,
  modifier: Modifier = Modifier,
  tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)

Icon’s main feature is it allows you to set a vector of the ImageVector type, which serves as an icon. There are multiple implementations of Icon, which allow you to provide different types of assets, such as ImageBitmap and Painter!

Now you’ve finished the FloatingActionButton, build and run the app to see the result.

Action Button
Action Button

Your floating action button appears with a favorite icon in the shape of a heart. When you click it, it produces a ripple effect. You can also notice it has a small shadow underneath it, because of its elevation.

The buttons look awesome, and you’ve learned a lot about them! Nice job! :]

More Buttons to Use

Here’s a brief overview of the other types of buttons in Jetpack Compose:

  • IconButton: Similar to a floating action button but without the floating part — it has no elevation. It’s commonly used for navigation.
  • OutlinedButton: Similar to an OutlinedTextField, this offers additional functionality like borders.
  • IconToggleButton: Has two states for icons that you can toggle on and off.
  • TextButton: Most commonly found in cards and dialogs, use this button for less pronounced actions.

After learning about all those buttons, you’re ready to move on and discover new elements.

Progress Bars

When you perform long operations like fetching data from a server or a database, it’s good practice to show a progress bar. The progress bar reduces the feeling of waiting too long by displaying an animation, and it gives the user a sense something is happening.

When you only want the user to know that work is taking place, spinning animated progress bars are a good choice.

In cases where you want to track progress and show the user how close they are to finishing the work, you want a progress bar that fills with a color as the progress occurs. This is very common when downloading or uploading files!

Jetpack Compose offers solutions to handle both cases. Open ProgressIndicatorScreen.kt and notice there’s only one composable function in this file:

@Composable
fun ProgressIndicatorScreen() {

  Column(
      modifier = Modifier.fillMaxSize(),
      horizontalAlignment = Alignment.CenterHorizontally,
      verticalArrangement = Arrangement.Center
  ) {
     //TODO add your code here
  }

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}

That’s because it’s so easy to display progress bars with Jetpack Compose that you don’t need additional custom composable functions.

Try it out by adding one circular and one linear progress bar inside Column() like so:

Column(
   modifier = Modifier.fillMaxSize(),
   horizontalAlignment = Alignment.CenterHorizontally,
   verticalArrangement = Arrangement.Center
) {
 CircularProgressIndicator(
     color = colorResource(id = R.color.colorPrimary),
     strokeWidth = 5.dp
 )
 LinearProgressIndicator(progress = 0.5f)
}

The column should stay as-is — it’s only there to position the elements inside it, and to center them.

You’re building both types of progress indicators here. First, you build the CircularProgressIndicator, defining an indicator color and a strokeWidth. These properties serve as styling. You don’t have to define the animation yourself, it’s already pre-baked into the component!

Then, you build the LinearProgressIndicator, and you set its progress to be 50%. Usually, you’d update this progress as your operations are computed within the system, but for the sake of simplicity, you’ll make it static for this exercise.

Exploring the Progress Indicators

Since these are really simple components to implement, they also have very simple definitions. Open the CircularProgressIndicator signature, and you’ll see the following:

@Composable
fun CircularProgressIndicator(
  progress: Float,
  modifier: Modifier = Modifier,
  color: Color = MaterialTheme.colors.primary,
  strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth
)

This function offers a small range of styling. The most important parameter is the progress, which ranges from 0.0 to 1.0 — the number determines the filled ratio of the progress bar. If you don’t set the progress, the progress bar will run an infinite spinning animation.

The other styling options change the color and the stroke width. The default stroke width is 4 dp.

On the other hand, the LinearProgressIndicator signaturelooks like this:

@Composable
fun LinearProgressIndicator(
  /*@FloatRange(from = 0.0, to = 1.0)*/
  progress: Float,
  modifier: Modifier = Modifier,
  color: Color = MaterialTheme.colors.primary,
  backgroundColor: Color = color.copy(alpha = IndicatorBackgroundOpacity)
)

The options are almost the same, except it doesn’t offer the ability to change the stroke width. Though you usually use a linear progress bar to indicate static progress, you can also use it with an infinite animation by not setting the progress parameter. The animation will then go from left to right until the operation completes.

Now you’ve explored these progress bars, build and run the app, then open the Progress screen from the navigation menu:

Progress Bars
Progress Bars

You can see two progress bars on the screen. The circular one is always spinning in animation while the linear one stays static at the halfway point.

This example shows how simple Jetpack Compose makes it to implement the most common features you use while making apps!

Now, it’s time to learn about a more complex element, where you’ll have to handle states and actions.

AlertDialog

The next composable function you’ll implement is an AlertDialog. Dialogs are used to alert the user about an action, or to request confirmation. For example, you can use a dialog to confirm whether the user wants to delete an item, request they rate the app and so on. They are very common in apps, and are used across all operating systems — not just Android!

The most important part of working with a dialog is to handle the state that determines when to show or dismiss that dialog. You’ll start by adding an alert dialog that has only one button: Confirm. The dialog will close when the user clicks the button or clicks outside the dialog.

To implement this behavior, open AlertDialogScreen.kt and change code inside MyAlertDialog to the following:

@Composable
fun MyAlertDialog() {
  val shouldShowDialog = remember { mutableStateOf(true) } // 1

  if (shouldShowDialog.value) { // 2
    AlertDialog( // 3
      onDismissRequest = { // 4
        shouldShowDialog.value = false
        JetFundamentalsRouter.navigateTo(Screen.Navigation)
      },
      // 5
      title = { Text(text = stringResource(id = R.string.alert_dialog_title)) },
      text = { Text(text = stringResource(id = R.string.alert_dialog_text)) },
      confirmButton = { // 6
        Button(
          colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.colorPrimary)),
          onClick = {
            shouldShowDialog.value = false
            JetFundamentalsRouter.navigateTo(Screen.Navigation)
          }
        ) {
          Text(
            text = stringResource(id = R.string.confirm),
            color = Color.White
          )
        }
      }
    )
  }
}

That is a lot of code you had to add, but it’s mostly using components you’ve previously encountered. Let’s go through it step-by-step:

  1. You add a state representing whether to show the dialog or not, and sets the initial state to true.
  2. Using an if statement, you add logic to display the AlertDialog only if the state value is true. Because Compose renders the UI by calling functions, if the value is false, it won’t call the function — and in turn, it won’t display the dialog!
  3. Using AlertDialog(), you create your dialog, which has a title, a text message, a dismiss request handler, and a confirmButton().
  4. In onDismissRequest, you change the state of the dialog to dismiss it, then tell Navigation to return to the main navigation screen. JetFundamentalsRouter is a pre-baked class used for navigation. You need to call navigateTo and add the screen you want to go to as a parameter.
  5. You set the title and text as two Text()s and use the provided stringResources() to fill it.
  6. Finally, you add a Button() as the confirmButton. Clicking the button dismisses the dialog and navigates to the main navigation screen, just like in onDismissRequest(). You add a Text() to display the text inside the button with a white color and a predefined string resource.

Now you’ve prepared the dialog, build and run the app. On the navigation menu, select the Alert Dialog screen.

Alert Dialog
Alert Dialog

Upon opening the screen, an alert dialog automatically appears. It has a basic title and the text you set. Clicking outside the dialog or inside the confirm button dismisses the dialog and returns you to the previous screen.

Implementing the alert dialog might have looked complicated due to the code size, but most of the code only dealt with styling the alert and handling click events.

Dialogs are easy to create in Jetpack Compose, but keep in mind that you have to handle the state, which requires more effort when you want to reuse dialogs on multiple screens.

Exploring AlertDialog

It’s important to note that the AlertDialog composable you used comes from the androidx.compose.material package, meaning, it is built using the Material Design specs. There are several types of dialogs but the most common type is the AlertDialog you used, so open its signature to see what it can do:

@Composable
fun AlertDialog(
  onDismissRequest: () -> Unit,
  confirmButton: @Composable () -> Unit,
  modifier: Modifier = Modifier,
  dismissButton: @Composable (() -> Unit)? = null,
  title: @Composable (() -> Unit)? = null,
  text: @Composable (() -> Unit)? = null,
  shape: Shape = MaterialTheme.shapes.medium,
  backgroundColor: Color = MaterialTheme.colors.surface,
  contentColor: Color = contentColorFor(backgroundColor),
  properties: DialogProperties = DialogProperties()
)

There are new parameters to go through here:

  • onDismissRequest: Executes when a user clicks outside the dialog or presses the Back button.
  • confirmButton: A button that confirms a proposed action. It’s usually a TextButton.
  • dismissButton: This button dismisses an action. It’s also usually a TextButton.
  • title: Sets the title text with a composable function.
  • text: Sets the text inside the dialog with a composable function.
  • contentColor: The color used by elements within the AlertDialog.
  • properties: Platform-specific properties for further customization.

Throughout this chapter you saw how simple and straightforward Compose APIs are but most importantly how you can mix and match different Composables, slot them together and build larger, more complex pieces of UI.

This is the power of this new framework, it abstracts away the unnecessary complexity and lets you focus on the important bits. Over the course of the next few chapters you will learn how to harness this capability even further to build more complex screens and UIs.

For now, great job going through all of these fundamental components and learning so much about Jetpack Compose! :]

Key Points

  • Create composable functions with @Composable annotation.
  • Use setContent() inside an Activity as the root of your composable functions.
  • Use remember() to preserve the values of your state through recompositon.
  • Preview your composable functions by adding @Preview.
  • Text() displays a simple text.
  • TextField() allows you to retrieve input from a user. For more styling options, use OutlinedTextField().
  • Use Button() as the primary element of your app that handles click events.
  • Use RadioButton() as an element that the user can select. To make a group of radio buttons, you have to write the logic yourself.
  • Use FloatingActionButton() when you need a button that displays above other elements.
  • CircularProgressIndicator() and LinearProgressIndicator() allow you to either track progress or show a loading animation.
  • AlertDialog() is simple to use but requires state handling to work correctly.
  • Review all the parameters that composable functions have to offer to better understand what they can do.
  • Use Icons and Color objects to access a list of predefined icons and colors prepared by the Jetpack Compose framework.

Where to Go From Here?

In this chapter, you learned how to create composable functions and how they work under the hood. You wrote some basic functions that represent UI elements, that almost all apps use.

If you want to learn more about different Material Design-based components, check out the official reference guide: https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary on the Android developer documentation website.

In the next chapter, you’ll learn how to use containers such as Column, Row, Box, and how to group and position different elements to create a more complex user interface!

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.
© 2026 Kodeco Inc.