Android Compose – An Introduction to Declarative Android UI Development

More and more developers are now switching to declarative programming frameworks as they can build interfaces without defining complex imperative mechanisms to change an app’s state. 

With the latest declarative tools at their disposal, developers need less time and code than ever to produce high-performance apps.

In response to the trend toward declarative development, Google released its Jetpack Compose toolkit in 2019. By understanding this powerful tool, developers can quickly bring their apps to life with less code, robust features, and intuitive Kotlin APIs. 

This article will explain how to install Compose and introduce several essential features.

What is Android Compose and Why Use It? 

Jetpack Compose is Android’s modern toolkit for building native UI. Based on declarative programming, Compose simplifies and accelerates UI development for Android devices. Below, I will list a few advantages Compose compared to the previous Android development tools:

  • Compose does not require that developers define their UI elements with XML files. 
  • Compose makes it easier to develop responsive user interfaces.
  • Compose provides better support for animations and transitions.
  • Compose is more efficient than the previous Android UI toolkit.
  • Compose code is more readable and, thus, supports better collaboration with other developers.

Android Compose Essentials

To begin developing with Compose, we need to understand its basic architecture and how to define the following:

  • Layouts
  • Themes
  • Fonts
  • Navigation
  • Lists

First, we will begin by setting up a new Compose project.

Installation 

To begin a new Compose project, open Android Studio, create a new project and select Empty Compose Activity in the Project Template window. Configure the project with the settings below and add the following definitions and dependencies to your build.gradle file:

Min Gradle 7.0.0: classpath “com.android.tools.build:gradle:7.0.0” 

Kotlin version 1.5.31 : ext.kotlin_version = ‘1.5.31’ classpath 

“org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version” 

Min SDK version 21: minSdkVersion 21 

Add android compose to your project gradle: 

buildFeatures {

compose true 

compileOptions { 

sourceCompatibility JavaVersion.VERSION_1_8 

targetCompatibility JavaVersion.VERSION_1_8 

kotlinOptions { 

jvmTarget = “1.8” 

composeOptions { 

kotlinCompilerExtensionVersion ‘1.0.5’ 

  dependencies { 

//…… 

// Integration with activities 

implementation ‘androidx.activity:activity-compose:1.3.1’ 

// Compose Material Design 

implementation ‘androidx.compose.material:material:1.0.5’ 

// Animations 

implementation ‘androidx.compose.animation:animation:1.0.5’ 

// Tooling support (Previews, etc.) 

implementation ‘androidx.compose.ui:ui-tooling:1.0.5’ 

// Integration with ViewModels 

implementation ‘androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07’ // UI Tests 

androidTestImplementation ‘androidx.compose.ui:ui-test-junit4:1.0.5’ 

    } 

With our app configured, we can now discuss how to create layout elements in Compose.

Architecture 

As the Compose UI uses the declarative paradigm, we cannot alter layout elements directly as one does imperatively. Instead, a Compose UI maintains a tree of layout elements. We can change the state of an element, and Compose will redraw everything under it in the tree. To change the root layout element, you must update(redraw) every sub-element below.

Let’s say you have this simple compose UI with a container with a row with some text and a button. The widget tree has the container as the parent widget and the row as the container’s child. The row has two child elements: the text and the button. We can visualize this with the figure below.

If we change the Background-color of the container, the app redraws the entire tree from scratch. Redrawing the whole tree may demand considerable resources if the tree is large. So, we need to be careful with the changes we make. However, changing the row’s state will not affect the container, only the row’s children(the text and the button).

Besides the tree of layout elements, the Compose architecture is the same as the MVVM (Model View ViewModel) architecture. Only with Compose, the ViewModel updates the state and values of the layout elements in the tree, as illustrated below.

Compose Layouts 

We declare layouts in Compose as functions. In the example below, we create a profile card layout with a column containing two text elements:

@Composable 

fun ProfileCard() { 

Column { 

Text(“Mike Wasouski”) 

Text(“Android Developer & UI/UX Designer”) 

Note that composable functions always require the @Composable annotation before its definition.

Layout Types

We can divide the Compose layouts into two groups: data/interaction and containers. Data/interaction elements include image and text views, buttons, checkboxes, etc. Containers contain other layout elements and include items such as columns, rows, boxes, and constraints. 

We call the elements inside containers the children of the container. As mentioned in the architecture section, if we change the container element, the app recursively redraws every child element (and their children). 

We use containers to organize the views. Typically, we configure a view’s properties in the container element with settings such as background, touch behavior, margin, padding, etc. Let’s see an example: 

@Composable 

fun ProfileCard() { 

Row{ 

// getting the image from the drawable 

Image( 

modifier = Modifier 

.preferredSize(60.dp) 

.clip(CircleShape) 

.align(Alignment.CenterVertically), 

asset = imageResource(id = R.drawable.spikeysanju), 

contentScale = ContentScale.Crop 

// used to give horizontal spacing between components

Spacer(modifier = Modifier.width(16.dp)) 

Column(modifier = Modifier.align(Alignment.CenterVertically)) { 

Text( 

text = “Mike Wasouski”, 

color = MaterialTheme.colors.primary, 

style = MaterialTheme.typography.h6 

// used to give vertical spacing between components 

Spacer(modifier = modifier.height(8.dp)) 

Text( 

text = “Android Developer & UI/UX Designer”, 

color = MaterialTheme.colors.primaryVariant, 

style = MaterialTheme.typography.caption 

Notice, in the example above, my use of modifiers. Modifiers allow you to tweak how the app presents a composable layout and how the layout behaves. With modifiers, we can accomplish the following:

  1. Change the composable element’s behavior and appearance 
  2. Add information (e.g., accessibility labels) 
  3. Process user input 
  4. Add high-level interactions (e.g., make an element clickable, scrollable, draggable, or zoomable) 

Compose Theming 

Now that we have created some layout elements, we can customize their appearance with theming. Compose theming using Material design 3 (Material You), Google’s set of guidelines, tools, and components for Android UIs. We can model themes with classes containing specific theme values.

For instance, we can define a few colors with the code below: 

val Red = Color(0xffff0000) 

val Blue = Color(red = 0f, green = 0f, blue = 1f) 

private val DarkColors = darkColors( 

primary = Yellow200, 

secondary = Blue200,

// … 

private val LightColors = lightColors( 

primary = Yellow500, 

primaryVariant = Yellow400, 

secondary = Blue700, 

// … 

Then, we can set the color in the application’s theme: 

MaterialTheme( 

colors = if (darkTheme) DarkColors else LightColors ) { 

// app content 

Moreover, we can extend the theme by defining more specific values, like in the following code snippet: 

//Use with MaterialTheme.colors.snackbarAction val Colors.snackbarAction: Color 

get() = if (isLight) Red300 else Red700 

//Use with MaterialTheme.typography.textFieldInput val Typography.textFieldInput: TextStyle 

get() = TextStyle(/* … */) 

// Use with MaterialTheme.shapes.card 

val Shapes.card: Shape 

get() = RoundedCornerShape(size = 20.dp) 

Then, we can use the theme’s values in other elements: 

Text( 

text = “Hello theming”, 

color = MaterialTheme.colors.primary 

Also, you can define custom colors in your theme:

@Composable 

fun MyTheme( 

darkTheme: Boolean = isSystemInDarkTheme(), 

content: @Composable () -> Unit 

) { 

MaterialTheme( 

colors = if (darkTheme) DarkColors else LightColors, 

content = content 

@Composable 

fun AppDemo() { 

MyTheme( 

content = YourContentUnderThisTheme() 

If you need a specific resource for the theme, you can define it as I do below with the painterResource: 

val isLightTheme = MaterialTheme.colors.isLight 

//….. 

Icon( 

painterResource( 

id = if (isLightTheme) { 

R.drawable.ic_sun_24dp 

} else { 

R.drawable.ic_moon_24dp 

), 

contentDescription = “Theme” 

Compose Defining Fonts

In Compose, we can define and use fonts by simply creating a variable for the font, a variable for the typography of a layout, and adding the ladder to the theme. I will illustrate the process in the following example, which defines the font Rubik and adds it to my theme:

val Rubik = FontFamily( 

Font(R.font.rubik_regular), 

Font(R.font.rubik_medium, FontWeight.W500), 

Font(R.font.rubik_bold, FontWeight.Bold) 

val MyTypography = Typography( 

h1 = TextStyle( 

fontFamily = Rubik, 

fontWeight = FontWeight.W300, 

fontSize = 96.sp

), 

body1 = TextStyle( 

fontFamily = Rubik, 

fontWeight = FontWeight.W600, 

fontSize = 16.sp 

/*…*/ 

// Adding the typography to my theme. 

MaterialTheme(typography = MyTypography, /*…*/) 

Text( 

text = “Hello theming”, 

color = MaterialTheme.colors.primary, 

style = MaterialTheme.typography.h1 

Compose also allows developers to define specific themes for specific content. To learn more about custom theme configuration, see: Custom design systems in Compose – Jetpack.

Compose Navigation 

Compose dramatically simplifies how we handle navigation. We no longer need to use intents, and we don’t need to start a new activity or inflate fragments. 

With Compose, we do not need to pay attention to an activity’s lifecycle. Instead, developers can use the NavController API to handle navigation. Let’s see how this works with the following example: 

object MainDestinations { 

const val ROUTE_SIGN_IN = “sign_in” 

const val ROUTE_SIGN_UP = “sign_up” 

const val ROUTE_HOME = “home” 

lateinit varnavActions: MainActions 

@Composable 

fun AppNavGraph( 

navController: NavHostController =rememberNavController(),

startDestination: String = MainDestinations.ROUTE_SIGN_UP 

) { 

navActions=remember(navController){ MainActions(navController) } 

NavHost( 

navController = navController, 

startDestination = startDestination 

){ 

composable(MainDestinations.ROUTE_HOME){ HomeScreen() } 

composable(MainDestinations.ROUTE_SIGN_UP){ SingUpScreen() } 

composable(MainDestinations.ROUTE_SIGN_IN){ SingInScreen() } 

class MainActions(private val navController: NavHostController) { 

fun navigateTo(route: String, clear: Boolean = false) { 

with(navController){ 

if (clear) clearBackStack(route) else navigate(route) 

fun navigateUp() = navController.navigateUp() 

Compose Lists 

We can create static and dynamic view lists with Compose, as with previous XML implementations. However, Compose replaces RecyclerView and ScrollViews. So, in our examples, we will demonstrate how to create static and dynamic lists with Compose’s new tools.

Static Lists 

Intuitively, to create a static list, one group a view next to another (or beneath another) as in the code snippet below. Here, we implemented a loop that inserts a vertical list of Text elements: 

@Composable 

fun NonScrollableColumnDemo() { 

Column(){ 

for (i in 1..100) { 

Text( 

“User Name $i”, 

style = MaterialTheme.typography.h3,

modifier = Modifier.padding(10.dp) 

Divider(color = Color.Black, thickness = 5.dp) 

The scrollState variable retains the scroll position while the user navigates the app. When the user reaches this screen again, they will see the list in its place when they leave the view.

With the vertical scroll modifier added to our list, our code becomes:

@Composable 

fun ScrollableColumnDemo() { 

val scrollState = rememberScrollState() 

Column( 

modifier = Modifier.verticalScroll(scrollState) 

) { 

for (i in 1..100) { 

Text( 

“User Name $i”, 

style = MaterialTheme.typography.h3, 

modifier = Modifier.padding(10.dp) 

Divider(color = Color.Black, thickness = 5.dp) 

Dynamic lists (LazyColumn / LazyRow) 

Static lists perform better if we have a long list of views. For example, if we want to display hundreds of images, the app will require more memory and more time. Using the Compose equivalent of RecyclerView, LazyColumn, or LazyRow, we can create a dynamic list to display longer lists more efficiently.

As with other Compose features, LazyColumn and LazyRow are both easy to use. In the following code snippet, we will replace the Column from the previous example with a LazyColumn. Please pay attention to how we implement the items() function by indicating the number of elements we want to display:

@Composable 

fun LazyColumnDemo() { 

LazyColumn { 

items(count = 100){ 

Text( 

“User Name $it”, 

style = MaterialTheme.typography.h3, 

modifier = Modifier.padding(10.dp) 

Divider(color = Color.Black, thickness = 5.dp) 

In the LazyColumnDemo example, the app creates each item as it displays them. As the user scrolls past each element, the app erases them to save resources. If we want to store those items and recover their state, we can use the composable function remember() as in the example below:

@Composable 

fun LazyColumnDemo() { 

val numberList = remember { (0..99).toList() } 

LazyColumn { 

items( 

items = numberList, 

itemContent = { 

Text( 

“User Name $it”, 

style = MaterialTheme.typography.h3, 

modifier = Modifier.padding(10.dp) 

Divider(color = Color.Black, thickness = 5.dp) 

}

The Compose library simplifies dynamic list creation by removing the need to create an Adapter, a ViewHolder, and XML. The LazyColumn and LazyRow elements complete all this work for us.

Conclusion

Google’s Compose toolkit provides Android developers with intuitive and powerful declarative tools that improve app performance and require significantly less code. 

With the examples above, you can explore the essential features of Jetpack Compose and begin developing your apps with declarative programming. For more information, check out our support material below.

Support Material

Now that we have covered the basics, I recommend exploring the information, sample apps, and resources below to further develop your understanding of Jetpack Compose. 

Demo Project:

Sample apps and Documentation:

Want to work with Amaury Ricardo?
We are hiring :)

Interest in cryptocurrency today is greater than ever before. The opportunities to use crypto in everyday life range from buying […]
December 5
5 min read
When you think of design thinking, what comes to mind? You might imagine stock images of people in suits smiling […]
November 25
5 min read
Slicing and atomization have become popular terms in the software industry, but many developers may need help understanding their true […]
November 22
5 min read
In recent years, automating software quality tests has become widely popular in the tech industry. Current automation tools allow us […]
November 21
5 min read
While I was in a session with one of my mentors some time ago, we did an exercise called Ikigai, […]
November 18
5 min read