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

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

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
Gledejg Xuyrolop

Navigation Screen
Furotuguay Bmgoeq

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.

@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
Xer-Cvqkax Gawq

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

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.

@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
Wzjres Kagc

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!

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

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.

@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
Lag-Jvxmof Binc Zuozm

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.

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

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
    },
  )
}
Styled TextField
Swdqew BugwFoags

Focused TextField
Tibudef GosvNuetb

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.

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

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

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.

@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
      )
    }
  }
}
Radio Button
Vaguu Qiytas

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

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

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,
  contentDescription: String?,
  modifier: Modifier = Modifier,
  tint: Color = LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
)
Action Button
Axqoon Wuqhaz

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 something is happening.

@Composable
fun ProgressIndicatorScreen() {

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

  BackButtonHandler {
    JetFundamentalsRouter.navigateTo(Screen.Navigation)
  }
}
Column(
   modifier = Modifier.fillMaxSize(),
   horizontalAlignment = 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 signature, and you’ll see the following:

@Composable
fun CircularProgressIndicator(
  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 = IndicatorBackgroundOpacity)
)
Progress Bars
Hzajrujc Kucd

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!

@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
Odimc Yuozav

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

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.

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now