How To Draw A Jetpack Step By Step
Jetpack Compose is a modern toolkit for building native Android UI. Jetpack Compose simplifies and accelerates UI development on Android with less code, powerful tools, and intuitive Kotlin APIs.
In this tutorial, you'll build a simple UI component with declarative functions. You won't be editing any XML layouts or using the Layout Editor. Instead, you will call Jetpack Compose functions to say what elements you want, and the Compose compiler will do the rest.
-
4 lessons
Lesson 1: Composable functions
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.
Add a text element
To begin, download the most recent version of Android Studio Arctic Fox, and create an app using the Empty Compose Activity template. The default template already contains some Compose elements, but let's build it up step by step.
First, we'll 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()
function. The setContent
block defines the activity's layout where we call composable functions. 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()
function that is defined by the Compose UI library displays a text label on the screen.
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }
show preview
hide preview
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.
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard("Android") } } } @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") }
show preview
hide preview
Preview your function in Android Studio
Android Studio lets you preview your composable functions within the IDE, instead of installing the app to an Android device or emulator. The composable function must provide default values for any parameters. For this reason, you can't preview the MessageCard()
function directly. Instead, let's make a second function named PreviewMessageCard()
, which calls MessageCard()
with an appropriate parameter. Add the @Preview
annotation before @Composable
.
@Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }
show preview
hide preview
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. 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.
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { Text("Hello world!") } } }
show preview
hide preview
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard("Android") } } } @Composable fun MessageCard(name: String) { Text(text = "Hello $name!") }
show preview
hide preview
@Composable fun MessageCard(name: String) { Text(text = "Hello $name!") } @Preview @Composable fun PreviewMessageCard() { MessageCard("Android") }
show preview
hide preview
Lesson 2: 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 we've built our first composable function and preview! To discover more Jetpack Compose capabilities, we're going to build a simple messaging screen containing a list of messages that can be expanded with some animations.
Let's start by making our message composable richer by displaying the name of its author and a message content. We need first to change our 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:
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard(Message("Android", "Jetpack Compose")) } } } data class Message(val author: String, val body: String) @Composable fun MessageCard(msg: Message) { Text(text = msg.author) Text(text = msg.body) } @Preview @Composable fun PreviewMessageCard() { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) }
show preview
hide preview
This code creates two text elements inside the content view. However, since we 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.
@Composable fun MessageCard(msg: Message) { Column { Text(text = msg.author) Text(text = msg.body) } }
show preview
hide preview
Add an image element
Let's enrich our 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:
@Composable fun MessageCard(msg: Message) { Row { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", ) Column { Text(text = msg.author) Text(text = msg.body) } } }
show preview
hide preview
Configure your layout
Our 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. Let's use some of them to improve the layout:
@Composable fun MessageCard(msg: Message) { // Add padding around our message Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", modifier = Modifier // Set image size to 40 dp .size(40.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(8.dp)) Column { Text(text = msg.author) // Add a vertical space between the author and message texts Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
show preview
hide preview
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MessageCard(Message("Android", "Jetpack Compose")) } } } data class Message(val author: String, val body: String) @Composable fun MessageCard(msg: Message) { Text(text = msg.author) Text(text = msg.body) } @Preview @Composable fun PreviewMessageCard() { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) }
show preview
hide preview
@Composable fun MessageCard(msg: Message) { Column { Text(text = msg.author) Text(text = msg.body) } }
show preview
hide preview
@Composable fun MessageCard(msg: Message) { Row { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", ) Column { Text(text = msg.author) Text(text = msg.body) } } }
show preview
hide preview
@Composable fun MessageCard(msg: Message) { // Add padding around our message Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = "Contact profile picture", modifier = Modifier // Set image size to 40 dp .size(40.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(8.dp)) Column { Text(text = msg.author) // Add a vertical space between the author and message texts Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
show preview
hide preview
Lesson 3: 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 widgets.
Use Material Design
Our message design now has a layout, but it doesn't look too good yet.
Jetpack Compose provides an implementation of Material Design and its UI elements out of the box. We'll improve the appearance of our MessageCard composable using Material Design styling.
To start, we wrap our MessageCard
function with the Material theme created in your project, ComposeTutorialTheme
in this case. Do it both in the @Preview
and in the setContent
function.
Material Design is built around three pillars: Color, Typography, Shape. Let's add them one by one
Note: the Empty Compose Activity generates a default theme for your project that allows you to customize MaterialTheme. If you named your project anything different from ComposeTutorial, you can find your custom theme in the ui.theme package.
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { MessageCard(Message("Android", "Jetpack Compose")) } } } } @Preview @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!") ) } }
show preview
hide preview
Color
Styling with colors from the wrapped theme is easy, and you can use values from the theme anywhere a color is needed.
Let's style the title and add a border to the image:
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant ) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
show preview
hide preview
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 = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.body2 ) } } }
show preview
hide preview
Shape
With Shape we can add the final touches. We also add a padding to the message for a better layout.
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), style = MaterialTheme.typography.body2 ) } } } }
show preview
hide preview
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 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.
Let's add a new preview annotation and enable night mode.
@Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) } }
show preview
hide preview
Color choices for the light and dark themes are defined in the IDE-generated Theme.kt
file.
So far, we'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!
@Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) } }
show preview
hide preview
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { MessageCard(Message("Android", "Jetpack Compose")) } } } } @Preview @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!") ) } }
show preview
hide preview
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant ) Spacer(modifier = Modifier.height(4.dp)) Text(text = msg.body) } } }
show preview
hide preview
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Text( text = msg.body, style = MaterialTheme.typography.body2 ) } } }
show preview
hide preview
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape) ) Spacer(modifier = Modifier.width(8.dp)) Column { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) { Text( text = msg.body, modifier = Modifier.padding(all = 4.dp), style = MaterialTheme.typography.body2 ) } } } }
show preview
hide preview
@Preview(name = "Light Mode") @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, name = "Dark Mode" ) @Composable fun PreviewMessageCard() { ComposeTutorialTheme { MessageCard( msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!") ) } }
show preview
hide preview
Lesson 4: 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 let's change our conversation to have more than one message. We need to create a Conversation
function that will show multiple messages. For this use case, we can 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. At the same time, they avoid the complexity of RecyclerView
with XML layouts.
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 this sample dataset into your project to help bootstrap the conversation quickly:
import androidx.compose.foundation.lazy.items @Composable fun Conversation(messages: List<Message>) { LazyColumn { items(messages) { message -> MessageCard(message) } } } @Preview @Composable fun PreviewConversation() { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } }
show preview
hide preview
Animate messages while expanding
Our conversation is getting more interesting. It's time to play with animations! We 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, we need to keep track of whether a message has been extended or not. To keep track of this state change, we 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 its children) using this state will get redrawn automatically when the value is updated. We call this recomposition.
By using Compose's state APIs like remember
and mutableStateOf
, any changes to state automatically update the UI:
Note: You will need to add the following imports to correctly use `by`. Alt+Enter will add them for you.
import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } } } } @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape) ) Spacer(modifier = Modifier.width(8.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.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, elevation = 1.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.body2 ) } } } }
show preview
hide preview
Now we can change the background of the message content based on isExpanded
when we click on a message. We will use the clickable
modifier to handle click events on the composable. Instead of just toggling the background color of the Surface
, we will animate the background color by gradually modifying its value from MaterialTheme.colors.surface
to MaterialTheme.colors.primary
and vice versa. To do so, we will use the animateColorAsState
function. Lastly, we will use the animateContentSize
modifier to animate the message container size smoothly:
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape) ) Spacer(modifier = Modifier.width(8.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: Color by animateColorAsState( if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface, ) // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, elevation = 1.dp, // surfaceColor color will be changing gradually from primary to surface color = surfaceColor, // animateContentSize will change the Surface size gradually modifier = Modifier.animateContentSize().padding(1.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.body2 ) } } } }
show preview
hide preview
import androidx.compose.foundation.lazy.items @Composable fun Conversation(messages: List<Message>) { LazyColumn { items(messages) { message -> MessageCard(message) } } } @Preview @Composable fun PreviewConversation() { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } }
show preview
hide preview
class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ComposeTutorialTheme { Conversation(SampleData.conversationSample) } } } } @Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape) ) Spacer(modifier = Modifier.width(8.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.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, elevation = 1.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.body2 ) } } } }
show preview
hide preview
@Composable fun MessageCard(msg: Message) { Row(modifier = Modifier.padding(all = 8.dp)) { Image( painter = painterResource(R.drawable.profile_picture), contentDescription = null, modifier = Modifier .size(40.dp) .clip(CircleShape) .border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape) ) Spacer(modifier = Modifier.width(8.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: Color by animateColorAsState( if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface, ) // We toggle the isExpanded variable when we click on this Column Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) { Text( text = msg.author, color = MaterialTheme.colors.secondaryVariant, style = MaterialTheme.typography.subtitle2 ) Spacer(modifier = Modifier.height(4.dp)) Surface( shape = MaterialTheme.shapes.medium, elevation = 1.dp, // surfaceColor color will be changing gradually from primary to surface color = surfaceColor, // animateContentSize will change the Surface size gradually modifier = Modifier.animateContentSize().padding(1.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.body2 ) } } } }
show preview
hide preview
Next steps
Congratulations, you've finished the Compose tutorial! You've built a simple chat screen efficiently showing a list of expandable & animated messages containing an image and texts, designed using Material Design principles with a dark theme included and previews—all in under 100 lines of code!
Here's what you've learned so far:
- Defining composable functions
- Adding different elements in your composable
- Structuring your UI component using layout composables
- Extending composables by using modifiers
- Creating an efficient list
- Keeping track of state and modifying it
- Adding user interaction on a composable
- Animating messages while expanding them
If you want to dig deeper on some of these steps, explore the resources below.
Continue your learning
How To Draw A Jetpack Step By Step
Source: https://developer.android.com/jetpack/compose/tutorial
Posted by: alstonorwits.blogspot.com
0 Response to "How To Draw A Jetpack Step By Step"
Post a Comment