Jetpack Compose

Oct 11 2022 · Kotlin 1.7.10, Android 13, Android Studio Chipmunk

Part 1: Jetpack Compose Basics

05. Decouple Composables

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 04. Add State to Composables Next episode: 06. Build Common UI Components - Part 1

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Notes: 05. Decouple Composables

In ui/addBook/AddBookActivity, within AddBookTopBar() composable function, the method onBackPressed() is deprecated and is replaced by onBackPressedDispatcher.onBackPressed() method.

In ui/composeUi/InputField, the statement backgroundColor = Color.White is added to make the background color of OutlinedTextField as white instead of the default theme color.

Transcript: 05. Decouple Composables

Demo

One of the most important things about writing good and clean code is decoupling different components, to make them more reusable.

So far you haven’t decoupled your components as much, but it’s time to change that! :] In this episode, you’ll be splitting up some of the components into reusable functions. Let’s get on it.

Start off by creating a package within the ui package, named composeUi.

[Create package]

This package will hold all of the reusable components in your app, such as top bars, buttons, input fields, and more.

Now create the following four files to the package: ActionButton, BackButton, InputField, TopBar.

[Add files]

As the names state, all of these will represent one of the main components in your app, that you’ll reuse over all of the screens that need them.

You’ll start by creating the ActionButton, so add the following code to the file:

@Composable
@Preview
fun ActionButton(
  modifier: Modifier = Modifier,
  text: String = "Librarian",
  isEnabled: Boolean = true,
  enabledColor: Color = colorResource(id = R.color.colorPrimary),
  disabledTextColor: Color = Color.Gray,
  onClick: () -> Unit = {}
) {

}

That’s the signature, now let’s add the implementation!

  val backgroundColor = if (isEnabled) enabledColor else Color.LightGray

  TextButton(
    shape = RoundedCornerShape(16.dp),
    enabled = isEnabled,
    colors = ButtonDefaults.textButtonColors(
      backgroundColor = backgroundColor,
      contentColor = Color.White,
      disabledContentColor = disabledTextColor
    ),
    modifier = modifier
      .padding(16.dp),
    content = { Text(text = text) },
    onClick = onClick
  )

There’s a lot of code here, but most of it is just styling!

You set up the parameters for the ActionButton, to take in a text, if the button was enabled or not, the colors, a modifier, for extra styling, and an onClick lambda.

If you’re using @Preview, you need to define all the default arguments, for the preview.

You proceed to build a TextButton, which has a rounded corner shape, lots of color styling, and a simple Text content.

Within the function, you’re setting the colors parameter using ButtonDefaults.textButtonColors().

This lets you define the content color, disabled content color and background color.

All of the styling and parameters will let you build ActionButtons simply throughout the app, following the same design system, so you don’t have to worry about having different types of buttons all around!

Also notice the DP function, that turns integers into scalable pixel sizes.

Now let’s move onto a second component, the BackButton:

@Composable
@Preview
fun BackButton(
  modifier: Modifier = Modifier,
  onBackAction: () -> Unit = {}
) {
  IconButton(
    modifier = modifier,
    content = {
      Icon(
        Icons.Default.ArrowBack,
        tint = Color.White,
        contentDescription = "Back"
      )
    },
    onClick = { onBackAction() }
  )
} 

The BackButton is simple too, it’s just an IconButton, using the ArrowBack icon from the Compose framework. But it’s easy to reuse now, as you only need to pass in an onClick lambda, and the rest of the code is predefined for you.

Let’s move onto the InputField. Add the following code to the file:

@Composable
@Preview
fun InputField(
  modifier: Modifier = Modifier,
  value: String = "",
  label: String = stringResource(id = R.string.app_name),
  keyboardType: KeyboardType = KeyboardType.Text,
  isInputValid: Boolean = true,
  imeAction: ImeAction = ImeAction.Next,
  onStateChanged: (String) -> Unit = {}
) {
  val focusedColor = colorResource(id = R.color.colorPrimary)
  val unfocusedColor = colorResource(id = R.color.colorPrimaryDark)

}

Again, you’ve added the signature for the InputField and you prepared some colors for the focused and unfocused states. The next step is to add the implementation of the component.

  OutlinedTextField(
    value = value,
    onValueChange = { newValue -> onStateChanged(newValue) },
    label = { Text(label) },
    modifier = modifier
      .fillMaxWidth()
      .padding(
        start = 16.dp,
        end = 16.dp,
        top = 4.dp,
        bottom = 4.dp
      ),
    keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction),
    visualTransformation = getVisualTransformation(keyboardType),
    isError = !isInputValid,
    colors = ...
  )

This is the meaty part of the component, where you define all the logic for displaying the text and reacting to text changes. You also added a label - also known as a hint, modifiers for padding, keyboard options and a visual transformation.

All of this helps the component be robust and reusable. Now add the colors to the component:

Note

colors = TextFieldDefaults.textFieldColors(
      focusedIndicatorColor = focusedColor,
      focusedLabelColor = focusedColor,
      unfocusedIndicatorColor = unfocusedColor,
      unfocusedLabelColor = unfocusedColor,
      cursorColor = focusedColor,
      backgroundColor = Color.White    
    )

These colors help you style your component in the color palette that we’re using for the rest of the project.

Finally, build the getVisualTransformation function.

private fun getVisualTransformation(keyboardType: KeyboardType) =
  if (keyboardType == KeyboardType.Password || keyboardType == KeyboardType.NumberPassword)
    PasswordVisualTransformation()
  else VisualTransformation.None

This is probably one of the most complex components you’ll build, but it’ll be worth it! The InputField has lots of input properties, such as the validity of the input, the keyboard type, imeAction, and onStateChanged lambda.

It’s using the OutlinedTextField component, styled to fill in the max width, with some extra padding, and it automatically builds all the colors and labels for you. Additionally, it applies the password visual transformation, if the keyboard type is a Password.

You should be able to follow along with the code, as it’s mostly what you’ve used so far!

And finally, let’s add the TopBar to its respective file:

@Composable
@Preview
fun TopBar(
  modifier: Modifier = Modifier,
  title: String = "Add a new review",
  actions: @Composable RowScope.() -> Unit = {},
  onBackPressed: (() -> Unit)? = null
) {

}

The TopBar is a really simple component with only a few parameters. Now finish it off by adding the following code:

  val backButtonAction: (@Composable () -> Unit)? = if (onBackPressed != null) {
    @Composable { BackButton(onBackAction = { onBackPressed() }) }
  } else {
    null
  }

This code is used to set up the back button, in case you provide an onBackPressed lambda function. If you don’t it just returns null, and you won’t show a back button.

  TopAppBar(
    modifier = modifier,
    title = { Text(title) },
    navigationIcon = backButtonAction,
    actions = actions,
    backgroundColor = colorResource(id = R.color.colorPrimary),
    contentColor = Color.White
  )

This should be pretty straightforward. The only thing that you need to understand is the backButtonAction. If there is no action for back navigation, then you don’t need to show the back button component.

Notice how you also added the actions parameter. This is a composable function, with the RowScope receiver. This means that the lambda composable function you pass in will be a Row component, so items will be ordered horizontally.

Good job adding all of these reusable components! Now let’s implement some of these in the files you’ve already built. Head over to the AddBookActivity class, and replace the code as such:

Note

@Composable
fun AddBookTopBar() {
  TopBar(
    title = stringResource(id = R.string.add_book_title),
    onBackPressed = { onBackPressedDispatcher.onBackPressed() })
}

It’s much easier to set up a TopBar now, that has all the UI features you need!

Also change the InputFields and the Button to match the new components:

      InputField(
        value = bookNameState.value,
        onStateChanged = { newValue ->
          bookNameState.value = newValue
          _addBookState.value = _addBookState.value?.copy(name = newValue)
        },
        label = stringResource(id = R.string.book_title_hint)
      )

      InputField(
        value = bookDescriptionState.value,
        onStateChanged = { newValue ->
          bookDescriptionState.value = newValue
          _addBookState.value = _addBookState.value?.copy(description = newValue)
        },
        label = stringResource(id = R.string.book_description_hint)
      )

      ActionButton(
        text = stringResource(id = R.string.add_book_button_text),
        onClick = { onAddBookTapped() }
      )

The code isn’t very different, but it’s going to be consistent, and as you change your color palette and design system later in the course, it’s going to reflect those changes!

Finally, head over to the BooksFragment, and change the code like so:

  @Composable
  fun BooksTopBar() {
    TopBar(
      title = stringResource(id = R.string.my_books_title),
      actions = { FilterButton() })
  }

  @Composable
  fun FilterButton() {
    IconButton(onClick = {
      // TODO
    }) {
      Icon(Icons.Default.Edit, tint = Color.White, contentDescription = "Filter")
    }
  }

The TopBar now lets you easily set up your UI, and add special actions, just as if you had a menu. You’re going to be building the FilterButton logic and UI in the next few episodes, but for now, you can prepare the basic component here to save time! :]

Now that you’ve prepared everything, you should be able to run the app.

[Build & run]

Awesome! Check out the AddBookActivity, and you’ll notice how the UI looks a bit nicer now! Your components are now reusable for all other screens, which will save you lots of time to develop the UI features of the app!