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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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
) { ... }

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.

Project Packages
Wnowezv Raksicih

Navigation Screen
Hujikefaus Stfaif

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.

@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
}
@Composable
fun MyText() {
 Text(text = )
}
stringResource(id = R.string.jetpack_compose)
Non-Styled Text
Jap-Rjlbud Boqd

@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()
)

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.

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.

@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
 )
}
Styled Text
Qdwmob Rayg

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(...)
}
Preview
Qtapuas

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.

@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
}
@Composable
fun MyTextField() {
  val textValue = remember { mutableStateOf("") }

  TextField(
    value = textValue.value,
    onValueChange = {
      textValue.value = it
    },
    label = {}
  )
}
Non-Styled Text Field
Rey-Pnsfuw Mapd Foevw

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.

@Composable
fun TextField(
    value: String,
    onValueChange: (String) -> Unit,
    label: @Composable () -> Unit,
    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
    onImeActionPerformed: (ImeAction, SoftwareKeyboardController?) -> Unit = { _, _ -> },
    ...
)

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
    },
  )
}
Styled TextField
Jlzpic KozjBiihx

Focused TextField
Nabiloh CotqNaeck

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.

@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
}

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
    )
  }
}
Button
Jictex

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

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.

@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
      )
    }
  }
}
Radio Button
Fequu Gityed

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()
)

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.

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

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
)
@Composable 
fun Icon(
  imageVector: ImageVector,
  modifier: Modifier = Modifier,
  tint: Color = AmbientContentColor.current.copy(alpha = AmbientContentAlpha.current)
)
Action Button
Iltoab Jaxquw

More buttons to use

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

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!

@Composable
fun ProgressIndicatorScreen() {

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

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

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
)
@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)
)
Progress Bars
Plebbuzy Yodg

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!

@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
          )
        }
      }
    )
  }
}
Alert Dialog
Ecehh Ceadon

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

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.

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.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now