Product Design
-
October 25, 2022

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 [.c-inline-code]build.gradle[.c-inline-code] 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 [.c-inline-code]@Composable[.c-inline-code] 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 = 0 f, green = 0 f, blue = 1 f)

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: