Jetpack Compose basics

scratch, Changing UI, Reusing, column, row, box, State, State Hoisting, Persisting state, List Animations, Styling & Theming

Jetpack Compose is a modern toolkit designed to simplify UI development. It combines a reactive programming model with the conciseness and ease of use of the Kotlin programming language. It is fully declarative, meaning you describe UI by calling a series of functions that transform data into a UI hierarchy. When the underlying data changes, the framework automatically re-execute these functions, updating the UI hierarchy for you.

A Compose app is made up of composable functions — just regular functions marked with , which can call other composable functions. A function is all you need to create a new UI component. The annotation tells Compose to add special support to the function for updating and maintaining UI over time. Compose lets you structure the code into small chunks. Composable functions are often referred to as "composables" for short.

By making small reusable composables, it’s easy to build up a library of UI elements used in app. Each one is responsible for one part of the screen and can be edited independently.

Prerequisites

  • Experience with Kotlin syntax, including lambdas

What you’ll need

Create a new app with support for Jetpack Compose

If you want to start a new project that includes support for Jetpack Compose by default, Android Studio includes new project templates to help you get started. To create a new project that include Jetpack Compose, proceed as follows:

  1. If you’re in the Welcome to Android Studio window, click Start a new Android Studio project. If you already have an Android Studio project open, select File > New > New Project from the menu bar.
  2. In the Select a Project Template window, select Empty Compose Activity and click Next.
  3. In the Configure your project window, do the following:
    - Set the Name, Package name, and Save location as you normally would.
    - Note that, in the Language dropdown menu, Kotlin is the only available option because Jetpack Compose works only with classes written in Kotlin.
    - In the Minimum API level dropdown menu, select API level 21 or higher.
  4. Click Finish.

Verify that the files is configured correctly
OR
If you want to use Jetpack Compose in an existing project, you’ll need to configure project with required settings and dependencies.

configure the Kotlin and Android Gradle Plugin that corresponds to the version of Android Studio:
Make sure you’re using Kotlin 1.5.21 in project

Configure Gradle:

You need to set app’s minimum API level to 21 or higher and enable Jetpack Compose in app’s file, as shown below. Also set the version for the Kotlin compiler plugin.

Add Jetpack Compose toolkit dependencies

After syncing the project, open and check out the code.

Note: The app theme used inside depends on how project is named. This lession assumes that the project is called JetpackComposeBasic. If you copy-paste code from this article, don't forget to update with the name of the theme available in the file.

Getting Started With Compose

Composable functions

A composable function is a regular function annotated with . This enables function to call other functions within it. You can see how the function is marked as . This function will produce a piece of UI hierarchy displaying the given input, . is a composable function provided by the library.

@Composable
private fun Greeting(name: String) {
Text(text = "Hello $name!")
}

Note: Composable functions are Kotlin functions that are marked with the annotation, as you can see in the code snippet above.

Compose in an Android app

With Compose, remain the entry point to an Android app. In project, is launched when the user opens the app (as it's specified in the file). You use to define layout, but instead of using an XML file as you'd do in the traditional View system, you call Composable functions within it.

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeBasicsTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Compose")
}
}
}
}
}

To see how the text displays on the screen, you can either run the app in an emulator or device, or use the Android Studio preview.
To use the Android Studio preview, you just have to mark any parameterless Composable function or functions with default parameters with the annotation and build project. You can already see a function in the file. You can have multiple previews in the same file and give them names.

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
ComposeBasicsTheme {
Greeting("Compose")
}
}

Lets change the UI

Let’s start by setting a different background color for the . You can do this by wrapping the composable with a . takes a color, so use .

Note: and are concepts related to Material Design, which is a design system created by Google to help you create user interfaces and experiences.

@Composable
private fun Greeting(name: String) {
Surface(color = MaterialTheme.colors.primary) {
Text (text = "Hello $name!")
}
}

The components nested inside will be drawn on top of that background color.

The Material components, such as , are built to make app experience better by taking care of common features that you probably want in app, such as choosing an appropriate color for text. We say Material is opinionated because it provides good defaults and patterns that are common to most apps. The Material components in Compose are built on top of other foundational components (in ), which are also accessible from app components in case you need more flexibility.

understands that, when the background is set to the color, any text on top of it should use the color, which is also defined in the theme.

Modifiers

Most Compose UI elements such as and accept an optional parameter. Modifiers tell a UI element how to lay out, display, or behave within its parent layout.

For example, the modifier will apply an amount of space around the element it decorates. You can create a padding modifier with .

Now, add padding to on the screen:

@Composable
fun Greeting(name: String) {
Surface(color = MaterialTheme.colors.primary) {
Text(
text = "Hello $name!",
modifier = Modifier.padding(24.dp)
)
}
}

There are dozens of modifiers which can be used to align, animate, lay out, make clickable or scrollable, transform, etc. For a comprehensive list, check out the List of Compose Modifiers.

Reusing Composable

Create a Composable called that includes the greeting.

@Composable
private fun MyComposable() {
Surface(color = MaterialTheme.colors.background) {
Greeting("Compose")
}
}

This lets you clean up the callback and the preview as you can now reuse the composable, avoiding code duplication. file should look like this:

Columns and Rows

The three basic standard layout elements in Compose are , and .

They are Composable functions that take Composable content, so you can place items inside. For example, each child inside of a will be placed vertically.

Column {
Text("First row")
Text("Second row")
}

Now try to change so that it shows a column with two text elements like in this example:

@Composable
fun Greeting(name: String) {
Surface(color = MaterialTheme.colors.primary) {
Column(modifier = Modifier.padding(24.dp)) {
Text(text = "Hello,")
Text(text = name)
}
}
}

Composable functions can be used like any other function in Kotlin. This makes building UIs really powerful since you can add statements to influence how the UI will be displayed.

For example, you can use a loop to add elements to the :

@Composable
private fun MyComposable(names: List<String> = listOf("Basics", "Compose")) {
Column {
for (name in names) {
Greeting(name = name)
}
}
}

You haven’t set dimensions or added any constraints to the size of the composables yet, so each row takes the minimum space it can and the preview does the same thing. Let’s change the preview to emulate a common width of a small phone, 320dp. Add a parameter to the annotation like so:

@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
ComposeBasicsTheme {
MyComposable()
}
}

Modifiers are used extensively in Compose so let’s practice with a more advanced exercise: Try to replicate the following layout using the and modifiers.


@Composable
private fun MyComposable(names: List<String> = listOf("Basics", "Compose")) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
for (name in names) {
Greeting(name = name)
}
}
}

@Composable
fun Greeting(name: String) {
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
Text(text = "Hello, ")
Text(text = name)
}
}
}

Note that:

  • Modifiers can have overloads so, for example, you can specify different ways to create a padding.
  • To add multiple modifiers to an element, you simply chain them.

Adding a button

In the next step you’ll add a clickable element that expands the , so we need to add that button first. The goal is to create the following layout:

is a composable provided by the material package which takes a composable as the last argument. Since trailing lambdas can be moved outside of the parentheses, you can add any content to the button as a child. For example, a :

Button(
onClick = { }
) {
Text("Show less")
}

Note: Compose provides different types of according to the Material Design Buttons spec, , and . In this case, we'll use an that wraps a as the content.

To achieve this you need to learn how to place a composable at the end of a row. There’s no modifier so, instead, you give some to the composable at the start. The modifier makes the element fill all available space, making it flexible, effectively pushing away the other elements that don't have a weight, which are called inflexible. It also makes the modifier redundant.

Now try to add the button and place it as shown in the previous image.

Check out the solution here:

@Composable
fun Greeting(name: String) {
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { /* TODO */ }
) {
Text("Show more")
}
}
}
}

State in Compose

So far you’ve created static layouts but now you’ll make them react to user changes to achieve this:

Before getting into how to make a button clickable and how to resize an item, you need to store some value somewhere that indicates whether each item is expanded or not–the state of the item. Since we need to have one of these values per greeting, the logical place for it is in the composable. Take a look at this boolean and how it's used in the code:

@Composable
fun Greeting(name: String) {
val expanded = remember { mutableStateOf(false) }

Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded.value = !expanded.value },
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}

Note that we also added an action and a dynamic button text. More on that later.

However, this won’t work as expected. Setting a different value for the variable won't make Compose detect it as a state change so nothing will happen.

Compose apps transform data into UI by calling composable functions. If the data changes, Compose re-executes these functions with the new data, creating an updated UI — this is called recomposition. Compose also looks at what data is needed by an individual composable so that it only needs to recompose components whose data has changed and skip recomposing those that are not affected.

As mentioned in Thinking in Compose:

Composable functions can execute frequently and in any order, you must not rely on the ordering in which the code is executed, or on how many times this function will be recomposed.

The reason why mutating this variable does not trigger recompositions is that it’s not being tracked by Compose. Also, each time is called, the variable will be reset to false.

To add internal state to a composable, you can use the function, which makes Compose recompose functions that read that .

and are interfaces that hold some value and trigger UI updates (recompositions) whenever that value changes.

However you can’t just assign to a variable inside a composable. As explained before, recomposition can happen at any time which would call the composable again, resetting the state to a new mutable state with a value of .

To preserve state across recompositions, remember the mutable state using .

is used to guard against recomposition, so the state is not reset.

Note that if you call the same composable from different parts of the screen you will create different UI elements, each with its own version of the state. You can think of internal state as a private variable in a class.

The composable function will automatically be “subscribed” to the state. If the state changes, composables that read these fields will be recomposed to display the updates.

Mutating state and reacting to state changes

In order to change the state, you might have noticed that has a parameter called but it doesn't take a value, it takes a function.

If you’re not familiar with functions being used this way, it is a very powerful Kotlin feature that is used extensively in Compose. Functions are first class citizens in Kotlin, so they can be assigned to a variable, passed into other functions and even be returned from them. You can read how Compose uses Kotlin features here.

To learn more about how functions are defined and how you can instantiate them, read the Function Types documentation.

You can define the action to take on click by assigning a lambda expression to it. For example, let’s toggle the value of the expanded state, and show a different text depending on the value.

OutlinedButton(
onClick = { expanded.value = !expanded.value },
) {
Text(
if (expanded.value) "Show less" else "Show more"
)
}

Code up to this point:

@Composable
private fun Greeting(name: String) {
var expanded = remember { mutableStateOf(false) }

Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(if (expanded.value) "Show less" else "Show more")
}
}
}
}

Expanding the item

Now let’s actually expand an item when requested. Add an additional variable that depends on the state:

@Composable
private fun Greeting(name: String) {
val expanded = remember { mutableStateOf(false) }
val extraPadding = if (expanded.value) 48.dp else 0.dp

Surface
(
color = MaterialTheme.colors.primary,
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded.value = !expanded.value }
) {
Text(
if (expanded.value) "Show less" else "Show more"
)
}
}
}
}

You don’t need to remember against recomposition because it depends on a state and it's doing a simple calculation.

And now we can apply a new padding modifier to the Column:

If you run on an emulator, you should see that each item can be expanded independently:

State hoisting

In Composable functions, state that is read or modified by multiple functions should live in a common ancestor — this process is called state hoisting. To hoist means to lift or elevate.

Making state hoistable avoids duplicating state and introducing bugs, helps reuse composables, and makes composables substantially easier to test. Contrarily, state that doesn’t need to be controlled by a composable’s parent should not be hoisted. The source of truth belongs to whoever creates and controls that state.

Add the following code to :

@Composable
fun WelcomeScreen() {
var shouldShowWelcomeScreen by remember { mutableStateOf(true) }

Surface {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Compose Basics!")
Button(
modifier = Modifier.padding(vertical = 24.dp),
onClick = { shouldShowWelcomeScreen = false }
) {
Text("Continue")
}
}
}
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun WelcomePreview() {
ComposeBasicsTheme {
WelcomeScreen()
}
}

his code contains a bunch of new features:

  • You have added a new composable called and also a new preview. If you build the project you'll notice you can have multiple previews at the same time. We also added a fixed height to verify that the content is aligned correctly.
  • can be configured to display its contents in the center of the screen.
  • The modifier can be used to align composables inside a row or a column.
  • is using a keyword instead of the . This is a property delegate that saves you from typing every time.
  • When the button is clicked, is set to , however you are not reading the state from anywhere yet.

Now, adding this new Welcome screen to app. to show it on launch and then hide it when the user presses “Continue”.

In Compose, don’t hide UI elements. Instead, simply don’t add them to the composition, so they’re not added to the UI tree that Compose generates. do this with simple conditional Kotlin logic. For example to show the welcome screen or the list of greetings you would do something like:

@Composable
fun MyComposable() {
if (shouldShowWelcomeScreen) { // Where does this come from?
WelcomeScreen()
} else {
Greetings()
}
}

However we don’t have access to . It's clear that we need to share the state that we created in with the composable.

Instead of somehow sharing the value of the state with its parent, we hoist the state–we simply move it to the common ancestor that needs to access it.

First, move the content of into a new composable called :

@Composable
private fun MyComposable() {
Greetings()
}

@Composable
private fun Greetings(names: List<String> = listOf("Basics", "Compose")) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
for (name in names) {
Greeting(name = name)
}
}
}

Now add the logic to show the different screens in , and hoist the state.

@Composable
private fun MyComposable() {
var shouldShowWelcomeScreen by remember { mutableStateOf(true) }

if (shouldShowWelcomeScreen) {
WelcomeScreen(/* TODO */)
} else {
Greetings()
}
}

We also need to share with the onboarding screen but we are not going to pass it directly. Instead of letting mutate the state, it would be better to let it notify us when the user clicked on the Continue button.

How do we pass events up? By passing callbacks down. Callbacks are functions that are passed as arguments to other functions and get executed when the event occurs.

Try to add a function parameter to the onboarding screen defined as so you can mutate the state from .

Solution:

@Composable
private fun MyComposable() {
var shouldShowWelcomeScreen by remember { mutableStateOf(true) }

if (shouldShowWelcomeScreen) {
WelcomeScreen(
onContinueClicked = { shouldShowWelcomeScreen = false }
)
} else {
Greetings()
}
}

@Composable
fun WelcomeScreen(onContinueClicked: () -> Unit) {

Surface {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Welcome to the Compose Basics!")
Button(
modifier = Modifier
.padding(vertical = 24.dp),
onClick = onContinueClicked
) {
Text("Continue")
}
}
}
}

By passing a function and not a state to we are making this composable more reusable and protecting the state from being mutated by other composables. In general, it keeps things simple. A good example is how the needs to be modified to call the now:

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun WelcomePreview() {
ComposeBasicsTheme {
WelcomeScreen(onContinueClicked = {})
}
}

Assigning to an empty lambda expression means "do nothing", which is perfect for a preview.

Performant lazy list

Now let’s make the names list more realistic. So far you have displayed two greetings in a . But, can it handle thousands of them?

Change the default list value in the parameters to use another list constructor which allows to set the list size and fill it with the value contained in its lambda (here represents the list index):

names: List<String> = List(1000) { "$it" }

This creates 1000 greetings, even the ones that don’t fit in the screen. Obviously this is not performant. You can try to run it on an emulator (warning: this code might freeze emulator).

To display a scrollable column we use a . renders only the visible items on screen, allowing performance gains when rendering a big list.

Note: and are equivalent to in Android Views.

In its basic usage, the API provides an element within its scope, where individual item rendering logic is written:

@Composable
private fun Greetings(names: List<String> = List(1000) { "$it" } ) {
LazyColumn(modifier = Modifier.padding(vertical = 4.dp)) {
items(items = names) { name ->
Greeting(name = name)
}
}
}

Note: Make sure you import as Android Studio will pick a different items function by default.

Note: doesn't recycle its children like . It emits new Composables as you scroll through it and is still performant, as emitting Composables is relatively cheap compared to instantiating Android .

Persisting state

App has a problem: if you run the app on a device, click on the buttons and then you rotate, the onboarding screen is shown again. The function works only as long as the composable is kept in the Composition. When you rotate, the whole activity is restarted so all state is lost. This also happens with any configuration change and on process death.

Instead of using you can use . This will save each state surviving configuration changes (such as rotations) and process death.

Now replace the use of in with :

var shouldShowWelcomeScreen by rememberSaveable { mutableStateOf(true) }

Run, rotate, change to dark mode or kill the process. The is not shown unless you have previously exited the app.

With around 100 lines of code so far, you were able to display a long and performant scrolling list of items each holding their own state. Also, as you can see, app has a perfectly correct dark mode without extra lines of code.

Animating list

In Compose, there are multiple ways to animate the UI: from high-level APIs for simple animations to low-level methods for full control and complex transitions. You can read about them in the documentation.

In this section you will use one of the low-level APIs but don’t worry, they can also be very simple. Let’s animate the change in size that we already implemented:

For this you’ll use the composable. It returns a State object whose will continuously be updated by the animation until it finishes. It takes a "target value" whose type is .

Create an animated that depends on the expanded state. Also, let's use the property delegate (the keyword):

@Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp
)
Surface(
color = MaterialTheme.colors.primary,
modifier = Modifier
.padding(vertical = 4.dp, horizontal = 8.dp)
) {
Row(modifier = Modifier.padding(24.dp)) {
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding)
) {
Text(text = "Hello, ")
Text(text = name)
}
OutlinedButton(
onClick = { expanded = !expanded }
) {
Text(if (expanded) "Show less" else "Show more")
}

}
}
}

Run the app and try the animation out.

Note: If you expand item number 1, you scroll away to number 20 and come back to 1, you’ll notice that 1 is back to the original size. You could save this data with if it were a requirement, but we are keeping the example simple.

takes an optional parameter that lets you customize the animation. Let's do something more fun like adding a spring-based animation:

@Composable
private fun Greeting(name: String) {

var expanded by remember { mutableStateOf(false) }

val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)

Surface(
...
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))

...

)
}

Note that we are also making sure that padding is never negative, otherwise it could crash the app.

The spec does not take any time-related parameters. Instead it relies on physical properties (damping and stiffness) to make animations more natural. Run the app now to try the new animation:

Any animation created with is interruptible. This means that if the target value changes in the middle of the animation, restarts the animation and points to the new value. Interruptions look especially natural with spring-based animations:

If you want to explore the different types of animations, try out different parameters for , different specs (, ) and different functions: or a different type of animation API.

Full code for this section

Styling and theming app

We didn’t style any of the composables so far and yet you got a decent default, including dark mode support! Let’s look into what and are.

If you open the file, you see that uses in its implementation:

@Composable
fun ComposeBasicsTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable() () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}

MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

is a composable function that reflects the styling principles from the Material design specification. That styling information cascades down to the components that are inside its , which may read the information to style themselves. In UI, we are already using as follows:

ComposeBasicsTheme {
MyComposable()
}

Because wraps internally, is styled with the properties defined in the theme. From any descendant composable you can retrieve three properties of : , and . Use them to set a header style one of the s:

Column(modifier = Modifier.weight(1f)) {
Text(text = "Hello, ")
Text(text = name, style = MaterialTheme.typography.h4)
}

The composable in the example above sets a new . You can create your own , or you can retrieve a theme-defined style by using , which is preferred. This construct gives you access to the Material-defined text styles, such as -, , , etc. In your example, you use the style defined in the theme.

Now build to see our newly styled text:

In general it’s much better to keep your colors, shapes and font styles inside a . For example, dark mode would be hard to implement if you hard-code colors and it would require a lot of error-prone work to fix.

However sometimes you need to deviate slightly from the selection of colors and font styles. In those situations it’s better to base your color or style on an existing one.

For this, you can modify a predefined style by using the function. Make the number extra bold:

Text(
text = name,
style = MaterialTheme.typography.h4.copy(
fontWeight = FontWeight.ExtraBold
)
)

This way if you need to change the font family or any other attribute of , you don't have to worry about the small deviations.

Now this should be the result in the preview window:

Changing app’s theme

You can find everything related to the current theme in the files inside the folder. For example, the default colors that we have been using so far are defined in .

Let’s start by defining new colors. Add these to :

val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)

Now assign them to the 's palette in :

private val LightColorPalette = lightColors(
surface = Blue,
onSurface = Color.White,
primary = LightBlue,
onPrimary = Navy
)

If you go back to and refresh the preview, you'll see the new colors:

However, you haven’t modified the dark colors yet. Before doing that, let’s set up the previews for it. Add an additional annotation to with :

Final code for

Finishing touches!

Replace button with an icon

  • Use the composable together with a child .
  • Use and , which are available in the artifact. Add the following line to dependencies in your file.
implementation "androidx.compose.material:material-icons-extended:$compose_version"
  • Modify paddings to fix alignment.
  • Add a content description for accessibility (see “Use string resources” below).

Use string resources

Content description for “Show more” and “show less” should be present and you can add them with a simple statement:

contentDescription = if (expanded) "Show less" else "Show more"

However, hard-coding strings is a bad practice and you should get them from the file.

You can use “Extract string resource” on each string, available in “Context Actions” in Android Studio to do this automatically.

Alternatively, open and add the following resources:

<string name="show_less">Show less</string>
<string name="show_more">Show more</string>

Showing more

The “Composem ipsum” text appears and disappears, triggering a change in size of each card.

  • Add a new to the Column inside that is displayed when the item is expanded.
  • Remove the and instead apply the modifier to the . This is going to automate the process of creating the animation, which would be hard to do manually. Also, it removes the need to .

Add elevation and shapes

  • You could use the modifier together with modifier to achieve the card look. However, there's a Material composable that does exactly that: .

Final code

Final Preview

Conclusion

In this article, I have tried to offer some Jetpack Compose Concepts and practices that you could follow to simplify Android UI development. I hope you have found them very helpful. If you think you have any more of such suggestions or have any queries in one we discussed here then please share them in the comments section below. I would appreciate the feedback.

Android Developer Passionate about Clean Code, Open Source, Mobile UX, and coffee.

Android Developer Passionate about Clean Code, Open Source, Mobile UX, and coffee.