Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

2. Learning Jetpack Compose Fundamentals
Written by Tino Balint

In this chapter, you’ll cover the basics of Jetpack Compose. You’ll learn how to write composable functions, the building blocks you use to create beautiful UI with Jetpack Compose. Then 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 you want to show on the screen becomes a composable function.

Composable functions

In the first chapter, you learned how using XML to make a 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.
  • Different sources can manage state.

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 use a different basic building block. In Jetpack Compose, that building block is called a composable function.

To make a composable function, you’d do something like this:

@Composable
fun MyComposableFunction() {
  // TODO
}

You need to annotate a function or expression with @Composable — a special annotation class. Any function annotated this way is also called a composable function, as you can compose it within other such 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 source code for the Composable annotation class looks like this:

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

You can see that 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 and 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 any Views or XML resources, it instead works with composable functions!

Setting the content

setContent() signature looks like this:

fun ComponentActivity.setContent(
   recomposer: Recomposer = Recomposer.current(),
   content: @Composable () -> Unit
) { ... }

You can see that setContent() is an extension function of ComponentActivity. Extension functions are functions that 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 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 that 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 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 for that element is called Text. Next, you’ll see how to add basic text elements to the UI.

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 simple composable function that uses a Column component to list out items in a vertical order. In that sense, a Column is just like a vertical LinearLayout!

It’s also using modifiers and two Column properties to style the Column and 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 place the its items in the Center of its parent.

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 a really neat and easy-to-use way to import strings, drawables, colors and other resources into your UI elements. Normally, to get a string from resources, you’d 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 that let you retrieve different types of resources. In this case, you’ll 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 that you’ve implement the basic Text(), it’s best to see what other functionality such elements expose. 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 = currentTextStyle()
)

It offers a wide range of parameters for different styles. The first, text, lets you set the text you want 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 that Column() used modifiers to fill the parent size, but as mentioned before, 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.

Text element parameters

  • 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 that you’ve displayed text in your UI, it’s time to style it to make it look nicer! :]

Styling your text

Now, you’re going to 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
 )
}

Once again, there are a few things happening:

  1. Using fontStyle, you make the text italicized.
  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. And 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.

Until 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 it hard to build a complex UI because you have to picture everything in your head. 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 that 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 you’re building bigger UI components.

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

But showing static text is a bit dull! Next, you’ll see how to implement an input field, so that the user 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 function counterpart 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! :]

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 that every time the user taps on a keyboard button, 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 critical look at the screen, you’ll see that the current TextField is very basic. It’s missing a hint and some 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: String,
    onValueChange: (String) -> Unit,
    label: @Composable () -> Unit,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    onImeActionPerformed: (ImeAction, SoftwareKeyboardController?) -> Unit = { _, _ -> },
    ...
)

Like the Text composable function, TexField 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.
  • onImeActionPerformed: A callback that triggers every time a user makes an input action which performs an ImeAction.

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

Feel free to explore more of these parameters and play around with them, but for now, 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("") }

  OutlinedTextField(
    label = { Text(text = stringResource(id = R.string.email)) },
    activeColor = colorResource(id = R.color.colorPrimary),
    keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Email),
    value = textValue.value,
    onValueChange = {
      textValue.value = it
    },
  )
}

An OutlinedTextField is just a styling TextField, as it uses a special internal function to draw and animate a border around the field and a description text.

To add a hint, or a label as it’s known in Compose, you used the label property and passed 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 activeColor. It changes the color of the label, bottom indicator and cursor when the text field is in focus. For the active color, you use the primary color from resources.

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 with raywenderlich.com’s famous green color. Nice! :]

Next, you’ll learn how to button elements 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 that you need to make a basic screen 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 different types of buttons.

Building a login button

First, you’ll make the basic button that 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 above, 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 pretty 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 or other code 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,
    ...
)

Read about what each of the most important parameters does 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.
  • 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

As you’d expect, the composable function you use to make radio buttons is named RadioButton. A radio button is a small, circular button that 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! :]

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 color = RadioButtonDefaults.colors( // 4
        selectedColor = colorResource(id = R.color.colorPrimary),
        unselectedColor = colorResource(id = R.color.colorPrimaryDark),
        disabledColor = Color.LightGray
      )

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

There are many steps you have to take to build a radio group:

  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 currently selected. It also selects the first button by default.
  3. Using a for 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, but 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 deep 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 high elevation that positions them above other content on the screen, as if they were floating in the air. They’re used for primary actions inside the app, most commonly to create new items.

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

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

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.

The Icons object contains some predefined and commonly used icons in the Android world. Similar to what the Color object does for colors, you can choose between Filled, Default, Outlined and other types of icons as well as predefined vectors, such as the Favorite, Add, ArrowBack and other vector assets.

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 most important thing to remember here is that a FloatingActionButton has an elevation, is clickable and you add a content to it by using another composable function. In most cases, you’ll want to use an Icon() for the content. The signature of Icon() is very simple:

@Composable 
fun Icon(
  imageVector: ImageVector,
  modifier: Modifier = Modifier,
  tint: Color = AmbientContentColor.current.copy(alpha = AmbientContentAlpha.current)
)

Icon’s main feature is that 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 that you’ve finished the FloatingActionButton, build and run the app to see the result.

Action Button
Action Button

Your floating action button now 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 top 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 that something is happening. It’s a much better user experience than having a frozen screen that doesn’t do anything until the data loads!

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(),
      horizontalGravity = 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(),
   horizontalGravity = 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 composable function, and you’ll see the following:

@Composable
fun CircularProgressIndicator(
   @FloatRange(from = 0.0, to = 1.0) 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 source code looks 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 = DefaultIndicatorBackgroundOpacity)
)

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 that 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 and easy 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 display a message to the user, usually asking for an action. For example, you can use a dialog to confirm whether the user wants to delete an item, request that 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
          )
        }
      }
    )
  }
}

There is a lot of code you had to add, but it’s mostly using the components you’ve previously encountered, such as text and button elements! Go through it step-by-step:

  1. You add a state that represents 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 that 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 also important to note that Jetpack compose uses Material Design dialogs. The most common type is 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)
)

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.

Great job going through all of these components and learning so much about Jetpack Compose! :]

Key points

  • Create composable functions with @Composable.
  • 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, but it’s also used as a child component in other composable functions.
  • 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 easy to use but requires state handling to work.
  • 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 existing 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.

One thing that’s currently missing is something to help you position elements on the screen. If you want to learn more about different Material Design-based components, check out the official reference guide on the Android developer documentation website.

In the next chapter, you’ll learn how to use containers such as the Column, Row, Box, and how to group 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.