Leave a rating/review
Notes: 02. Learn About Composable Functions
This course was originally recorded in 2020. It has been reviewed and all content and materials updated as of September 2022.
Demo
To understand how Jetpack Compose works, it’s best to start from Composable functions and the way Compose renders the UI.
Open the BooksFragment to start learning about Compose. Notice the ComposeView constructor call, and apply() following it.
[Select ComposeView()]
This code doesn’t do anything on its own, so let’s add the following code to start using Compose, that you’ll finish in a minute:
return ComposeView(requireContext()).apply {
setContent { // new code
BooksContent()
}
}
The ComposeView is a special type of View that’s compatible with Jetpack Compose. Within it, you have access to a special function called setContent(). setContent() is similar to Activity’s setContentView() , where you decide which View is going to represent the UI of the screen at hand.
Set content receives a single function parameter:
fun setContent(content: @Composable () -> Unit) {
shouldCreateCompositionOnAttachedToWindow = true
this.content.value = content
if (isAttachedToWindow) {
createComposition()
}
}
The parameter is a special type of a lambda function, which has the Composable annotation. By annotating the lambda, or any other function, you allow that function to use other composable functions from within. Similar to how you enable the use of coroutines and suspend functions, by adding the suspend modifier.
In this case you’re just passing in BooksContent(). You might think this is a special object or View type, but it will actually be another @Composable function! Having UpperCamelCased names for composable functions is the practice, so make sure you follow it!
Now define BooksContent() as follows:
@Composable
fun BooksContent() {
Scaffold(topBar = { BooksTopBar() },
floatingActionButton = { AddNewBook() }) {
}
}
You can see that composable functions are nothing special. They just have an extra annotation, that lets them call other Jetpack Compose functions from within. If you remove the annotation, you’ll get an error that you can’t use other composable functions, without BooksContent() being annotated.
[Show this behavior]
Also notice there is no return type in the function. This is because in Compose, you don’t return any values or Views. You instead call a series of functions, that are rendered in the order of calling them.
In this case, you’re calling Scaffold() which is a predefined Material Design component, that allows you to easily define a Toolbar, known as a topBar, a FloatingActionButton, as shown in the code above, and things like a side menu Drawer or a BottomDrawer.
In the example above, you added a TopBar to the Scaffold, and a FloatingActionButton, but you left the trailing lambda function empty.
If you check out the definition of the Scaffold function, you’ll notice there are so many parameters you can pass to the function:
fun Scaffold(
modifier: Modifier = Modifier,
scaffoldState: ScaffoldState = rememberScaffoldState(),
topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) },
floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End,
isFloatingActionButtonDocked: Boolean = false,
drawerContent: @Composable (ColumnScope.() -> Unit)? = null,
drawerGesturesEnabled: Boolean = true,
drawerShape: Shape = MaterialTheme.shapes.large,
drawerElevation: Dp = DrawerDefaults.Elevation,
drawerBackgroundColor: Color = MaterialTheme.colors.surface,
drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
drawerScrimColor: Color = DrawerDefaults.scrimColor,
backgroundColor: Color = MaterialTheme.colors.background,
contentColor: Color = contentColorFor(backgroundColor),
content: @Composable (PaddingValues) -> Unit
)
Remember how programming defines two types of combining state and behavior - composition and inheritance. Jetpack Compose, like the name states, favors Composition over inheritance, and its functions show that clearly.
You can set up things like FABs, drawers, bottom and top bars, and define different parameters that style the content of all of these components. But you’ll learn a bit more about that later.
Almost all composable functions have at least one function parameter named content that defines the rest of the UI within those components. Similar to how setContent() works.
Now let’s define the remaining two functions within the Scaffold.
First define the TopBar:
@Composable
fun BooksTopBar() {
TopAppBar(
title = { Text(stringResource(id = R.string.my_books_title)) },
backgroundColor = colorResource(id = R.color.colorPrimary),
contentColor = Color.White
)
}
A TopBar is just a Toolbar, which holds a title, it can have a BG and content color, and it can define navigation icons and action icons, which used to be represented with menus.
Open its definition, to see what you can pass in as parameters:
fun TopAppBar(
title: @Composable () -> Unit,
modifier: Modifier = Modifier,
navigationIcon: @Composable (() -> Unit)? = null,
actions: @Composable RowScope.() -> Unit = {},
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
elevation: Dp = AppBarDefaults.TopAppBarElevation
)
You can see that the TopAppBar defines the title composable, to define what you’re going to show as the Title of the app. In the UI Toolkit, you could only pass in a String, but because of Compose flexibility, you can pass in any Composable function.
You can even have Buttons, icons, and other Composables, and not just text! Really cool! :]
Also notice all the stringResource() and colorResource(). These functions are Compose helper functions that let you fetch resources from within each composable function.
[Slides - Compose Function Tree, example with three Text elements]
Each composable function follows the order in which other composable function calls are made. Basically, you can think of it as if you’re calling functions in order, and that’s the way the UI is represented, structurally.
Just like how you have XML, and in a LinearLayout items are one under the other, in order of definition. There are similar components in Compose, that you’ll learn about later in the course.
[Switch back to demo]
Finally, define AddNewBook(), as follows:
@Composable
fun AddNewBook() {
FloatingActionButton(
content = { Icon(Icons.Filled.Add, contentDescription = "Add Book") },
onClick = {
showAddBook()
},
)
}
It’s just a simple FloatingActionButton, represented by an icon. The Icon function is another composable, representing a simple icon or vector asset. The Icons.Filled.Add parameter is just one of the predefined icons you can use.
You have default, filled, rounded and two-toned icons, and all of the basic and most used icons defined. Here you’re using the Add icon, which is a small plus icon.
Also notice the contentDescription attribute for accessibility.
The FloatingActionButton’s definition is the following:
fun FloatingActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
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 can see that most of composable functions have similar parameters, and a similar number of them. The FAB defines a shape, BG and content color, elevation, an onClick function, an icon composable and a few other parameters.
It defines a modifier, like every composable, but you’ll learn about them later in the course! :]
You’re probably wondering: “How do I know my UI works, and looks like I want it to?” - Well, that’s a good question! Let’s build and run the app to see. :]
[Build & run]
You can see the BooksFragment now has a Toolbar as you defined it, and a FloatingActionButton! It was super easy to do that, right? And if you tap on the FAB, you’ll open a new screen, which is currently empty, but you’ll fill it in next!
You can also add the @Preview annotation to the function, to allow in-android-studio preview.
However, sometimes this feature is a bit buggy, but it’ll improve with the tooling! :]