Compose et permet definir la interfície gràfica mitjançant funcions descriptives.
Introducció
Jetpack Compose is built around composable functions. These functions let you define your app's UI programmatically by describing how it should look and providing data dependencies, rather than focusing on the process of the UI's construction (initializing an element, attaching it to a parent, etc.). To create a composable function, just add the @Composable
annotation to the function name.
Projecte de suport: https://gitlab.com/xtec/android/Compose
Projecte
Crea un projecte amb Kotlin Multiplatform web wizard:
-
Open the Kotlin Multiplatform wizard.
-
On the New project tab, change the project name to "Compose" and the project ID to "dev.xtec".
-
Selecciona les opcions Android, Desktop i Web.
-
Click the Download button and unpack the resulting archive.
Composable functions
Add a text element
Sobreescriu el fitxer composeApp/src/commonMain/kotlin/dev/xtec/App.kt
::
package dev.xtec
// ...
@Composable
@Preview
fun App() {
MaterialTheme {
Text("Hello World!")
}
}
Primer, mostra un "Hola món!" text afegint un element de text dins del mètode onCreate
. Per fer-ho, defineixes un bloc de contingut i crida a la funció compostable Text
. El bloc setContent
defineix el disseny de l'activitat on es criden les funcions composables. Les funcions composables només es poden cridar des d'altres funcions composables.
Aquest codi mostra el text "Hello World!".
First, display a “Hello world!” text by adding a text element inside the onCreate
method. You do this by defining a content block, and calling the Text
composable function. The setContent
block defines the activity's layout where composable functions are called. Composable functions can only be called from other composable functions.
Jetpack Compose uses a Kotlin compiler plugin to transform these composable functions into the app's UI elements. For example, the Text
composable function that is defined by the Compose UI library displays a text label on the screen.
Run your application on desktop
You can create a run configuration for running the desktop application as follows:
-
Select Run | Edit Configurations from the main menu.
-
Click the plus button and choose Gradle from the dropdown list.
-
In the Tasks and arguments field, paste this command:
composeApp:run
-
Click OK.
Now, you can use this configuration to run the desktop app in its own OS window:
TODO afegir imatge
Define a composable function
To make a function composable, add the @Composable
annotation.
To try this out, define a MessageCard
function which is passed a name, and uses it to configure the text element.
// ...
import androidx.compose.runtime.Composable
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard("David")
}
}
}
@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name")
}
Preview your function in Android Studio
The @Preview
annotation lets you preview your composable functions within Android Studio without having to build and install the app to an Android device or emulator. The annotation must be used on a composable function that does not take in parameters. For this reason, you can't preview the MessageCard
function directly. Instead, make a second function named PreviewMessageCard
, which calls MessageCard
with an appropriate parameter. Add the @Preview
annotation before @Composable
.
// ...
import androidx.compose.ui.tooling.preview.Preview
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard("Dav")
}
}
}
@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name")
}
@Preview
@Composable
fun PreviewMessageCard(){
MessageCard("Eva")
}
Rebuild your project. The app itself doesn't change, since the new PreviewMessageCard
function isn't called anywhere, but Android Studio adds a preview window which you can expand by clicking on the split (design/code) view. This window shows a preview of the UI elements created by composable functions marked with the @Preview
annotation. To update the previews at any time, click the refresh button at the top of the preview window.
Layouts
UI elements are hierarchical, with elements contained in other elements. In Compose, you build a UI hierarchy by calling composable functions from other composable functions.
Add multiple texts
So far you’ve built your first composable function and preview! To discover more Jetpack Compose capabilities, you’re going to build a simple messaging screen containing a list of messages that can be expanded with some animations.
Start by making the message composable richer by displaying the name of its author and a message content. You need to first change the composable parameter to accept a Message object instead of a String, and add another Text composable inside the MessageCard composable. Make sure to update the preview as well.
// ...
data class Message(val author: String, val body: String)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard(
Message(
"David", "No diguis blat fins que no el tinguis al sac i ben lligat"
)
)
}
}
}
@Composable
fun MessageCard(msg: Message) {
Text(text = msg.author)
Text(text = msg.body)
}
@Preview
@Composable
fun PreviewMessageCard() {
MessageCard(Message("Eva", "En el pot petit hi ha la bona confitura"))
}
This code creates two text elements inside the content view. However, since you haven't provided any information about how to arrange them, the text elements are drawn on top of each other, making the text unreadable.
Using a Column
The Column
function lets you arrange elements vertically. Add Column
to the MessageCard
function.
You can use Row
to arrange items horizontally and Box
to stack elements.
// ...
import androidx.compose.foundation.layout.Column
@Composable
fun MessageCard(msg: Message) {
Column {
Text(text = msg.author)
Text(text = msg.body)
}
}
Add an image element
Enrich your message card by adding a profile picture of the sender. Use the Resource Manager to import an image from your photo library or use this one.
Add a Row
composable to have a well structured design and an Image
composable inside it.
// ...
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.res.painterResource
@Composable
fun MessageCard(msg: Message) {
Row {
Image(
painter= painterResource(R.drawable.coyote),
contentDescription = "Contact profile picture"
)
Column {
Text(text = msg.author)
Text(text = msg.body)
}
}
}
Configure your layout
Your message layout has the right structure but its elements aren't well spaced and the image is too big!
To decorate or configure a composable, Compose uses modifiers. They allow you to change the composable's size, layout, appearance or add high-level interactions, such as making an element clickable. You can chain them to create richer composables. You'll use some of them to improve the layout.
// ...
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
@Composable
fun MessageCard(msg: Message) {
// Add padding around our message
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = "Contact profile picture",
modifier = Modifier
// Set image size to 80 dp
.size(80.dp)
// Clip image to be shaped as a circle
.clip(CircleShape)
)
// Add a horizontal space between the image and the column
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(text = msg.author)
// Add a vertical space between the author and message texts
Spacer(modifier = Modifier.height(8.dp))
Text(text = msg.body)
}
}
}
Material Design
Compose is built to support Material Design principles. Many of its UI elements implement Material Design out of the box. In this lesson, you'll style your app with Material Design widgets.
Use Material Design
Your message design now has a layout, but it doesn't look great yet.
Jetpack Compose provides an implementation of Material Design 3 and its UI elements out of the box. You'll improve the appearance of our MessageCard
composable using Material Design styling.
To start, wrap the MessageCard
function with the Material theme created in your project, ComposeTheme
, as well as a Surface
. Do it both in the @Preview
and in the setContent
function. Doing so will allow your composables to inherit styles as defined in your app's theme ensuring consistency across your app.
// ...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTheme {
Surface(modifier = Modifier.fillMaxSize()) {
MessageCard(
Message(
"David", "No diguis blat fins que no el tinguis al sac i ben lligat"
)
)
}
}
}
}
}
@Preview()
@Composable
fun PreviewMessageCard() {
ComposeTheme {
Surface (modifier = Modifier.fillMaxSize()) {
MessageCard(Message("Eva", "En el pot petit hi ha la bona confitura"))
}
}
}
Material Design is built around three pillars: Color
, Typography
, and Shape
. You will add them one by one.
Note. the Empty Compose Activity template generates a default theme for your project that allows you to customize MaterialTheme
.
Color
Use MaterialTheme.colorScheme
to style with colors from the wrapped theme. You can use these values from the theme anywhere a color is needed. This example uses dynamic theming colors (defined by device preferences). You can set dynamicColor
to false
in the MaterialTheme.kt
file to change this.
Style the title and add a border to the image.
// ...
import androidx.compose.foundation.border
import androidx.compose.material3.MaterialTheme
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(3.dp, MaterialTheme.colorScheme.primary, CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.height(8.dp))
Text(text = msg.body)
}
}
}
Typography
Material Typography styles are available in the MaterialTheme
, just add them to the Text
composables.
// ...
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(3.dp, MaterialTheme.colorScheme.primary, CircleShape)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = msg.body,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
Shape
With Shape
you can add the final touches. First, wrap the message body text around a Surface
composable. Doing so allows customizing the message body's shape and elevation. Padding is also added to the message for a better layout.
// ...
import androidx.compose.material3.Surface
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.coyote),
contentDescription = "Contact profile picture",
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(
3.dp, MaterialTheme.colorScheme.primary,
CircleShape
)
)
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
text = msg.author, color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Surface(shape = MaterialTheme.shapes.large, shadowElevation = 2.dp) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
Enable dark theme
Dark theme (or night mode) can be enabled to avoid a bright display especially at night, or simply to save the device battery. Thanks to the Material Design support, Jetpack Compose can handle the dark theme by default. Having used Material Design colors, text and backgrounds will automatically adapt to the dark background.
You can create multiple previews in your file as separate functions, or add multiple annotations to the same function.
Add a new preview annotation and enable night mode
// ...
import android.content.res.Configuration
@Preview(name = "Light Mode")
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
@Composable
fun PreviewMessageCard() {
ComposeTheme {
Surface {
MessageCard(Message("Eva", "En el pot petit hi ha la bona confitura"))
)
}
}
}
Color choices for the light and dark themes are defined in the IDE-generated Theme.kt
file.
So far, you've created a message UI element that displays an image and two texts with different styles, and it looks good both in light and dark themes!
Lists and animations
Lists and animations are everywhere in apps. In this lesson, you will learn how Compose makes it easy to create lists and fun to add animations.
Create a list of messages
A chat with one message feels a bit lonely, so we are going to change the conversation to have more than one message. You'll need to create a Conversation
function that will show multiple messages. For this use case, use Compose's LazyColumn
and LazyRow
. These composables render only the elements that are visible on screen, so they are designed to be very efficient for long lists.
In this code snippet, you can see that LazyColumn
has an items
child. It takes a List
as a parameter and its lambda receives a parameter we've named message
(we could have named it whatever we want) which is an instance of Message
. In short, this lambda is called for each item of the provided List
.
// ...
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
object Data {
val messages = listOf(
Message(
"Josep Pla",
"Antigament, el viatjar era un privilegi de gran senyor. Generalment, era la coronació normal dels estudis d'un home. Ara el viatjar s'ha generalitzat i democratitzat considerablement. Viatja molta gent"
),
Message(
"Josep Pla",
"Però, potser, les persones que viatgen per arrodonir i afermar la seva visió del món i dels homes són més rares avui que fa cent anys. "
),
Message(
"Josep Pla",
"En el nostre país hi ha tres pretextos essencials per a passar la frontera: la peregrinació a Lourdes, la lluna de mel i els negocis. Hom no pot tenir idea de la quantitat de gent del nostre país que ha estat a Lourdes. És incomptable. "
),
Message(
"Josep Pla",
"Fa trenta anys, les persones riques de Catalunya feien el viatge de noces a Madrid. Avui van a París o a Niça i de vegades a Itàlia. La lluna de mel, però, és un mal temps per veure res i per formar-se. No es poden pas fer dues coses importants a la vegada. El pitjor temps, potser, per a viatjar, de la vida, és la temporada de la lluna de mel."
),
)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTheme {
Conversation(Data.messages)
}
}
}
}
@Composable
fun Conversation(messages: List<Message>) {
LazyColumn {
items(messages) { message -> MessageCard(message) }
}
}
@Preview
@Composable
fun PreviewConversation() {
ComposeTheme {
Conversation(Data.messages)
}
}
Animate messages while expanding
The conversation is getting more interesting. It's time to play with animations! You will add the ability to expand a message to show a longer one, animating both the content size and the background color. To store this local UI state, you need to keep track of whether a message has been expanded or not. To keep track of this state change, you have to use the functions remember
and mutableStateOf
.
Composable functions can store local state in memory by using remember
, and track changes to the value passed to mutableStateOf
. Composables (and their children) using this state will get redrawn automatically when the value is updated. This is called recomposition.
By using Compose's state APIs like remember
and mutableStateOf
, any changes to state automatically update the UI.
// ...
import androidx.compose.foundation.clickable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.josep),
contentDescription = "Contact profile picture",
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(
3.dp, MaterialTheme.colorScheme.primary, CircleShape
)
)
Spacer(modifier = Modifier.width(16.dp))
// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }
// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Surface(shape = MaterialTheme.shapes.large, shadowElevation = 2.dp) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
Now you can change the background of the message content based on isExpanded
when we click on a message. You will use the clickable
modifier to handle click events on the composable.
Instead of just toggling the background color of the Surface, you will animate the background color by gradually modifying its value from MaterialTheme.colorScheme.surface
to MaterialTheme.colorScheme.primary
and vice versa.
To do so, you will use the animateColorAsState
function.
Lastly, you will use the animateContentSize
modifier to animate the message container size smoothly:
// ...
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize
@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 16.dp)) {
Image(
painter = painterResource(R.drawable.josep),
contentDescription = "Contact profile picture",
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(
3.dp, MaterialTheme.colorScheme.primary, CircleShape
)
)
Spacer(modifier = Modifier.width(16.dp))
// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }
// surfaceColor will be updated gradually from one color to the other
val surfaceColor by animateColorAsState(
if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
)
// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
Text(
text = msg.author,
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
Surface(shape = MaterialTheme.shapes.large, shadowElevation = 2.dp,
// surfaceColor color will be changing gradually from primary to surface
color = surfaceColor,
// animateContentSize will change the Surface size gradually
modifier = Modifier.animateContentSize().padding(2.dp)) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.bodyLarge
)
}
}
}
}
Continue learning
Check out the curated pathway of codelabs and videos that will help you learn and master Jetpack Compose:
Try Jetpack Compose sample apps
The fastest way to experiment with the capabilities of Jetpack Compose is by trying Jetpack Compose sample apps hosted on GitHub. To import a sample app project from Android Studio, proceed as follows:
- If you're in the Welcome to Android Studio window, select Import an Android code sample. If you already have an Android Studio project open, select File > New > Import Sample from the menu bar.
- In the search bar near the top of the Browse Samples wizard, type "compose".
- Select one of the Jetpack Compose sample apps from the search results and click Next.
- Either change the Application name and Project location or keep the default values.
- Click Finish.
Android Studio downloads the sample app to the path you specified and opens the project. You can then inspect MainActivity.kt
in each of the examples to see Jetpack Compose APIs such as crossfade animation, custom components, using typography, and displaying light and dark colors in the in-IDE preview.