Jetpack Compose: Declarative UI in Android

Jetpack Compose: Declarative UI in Android

Jetpack Compose is officially stated as Android’s modern toolkit for building native UI which simplifies and accelerates UI development on Android and, quickly brings apps to life with less code, powerful tools, and intuitive Kotlin APIs.

Google has introduced its latest toolkit Jetpack Compose for building Android applications and it is expected to replace Android View System. It is not the only available declarative UI concept. React Native, Flutter, and Swift UI all are based on the declarative UI concept.

Here, in this article, I am trying to help newcomers to understand the basics of Jetpack Compose using Android Studio. The stable android studio version while writing this article is Chipmunk. Let's hop on.

First Jetpack Compose Project

The best way to create compose project is through the New Project in Android Studio as in the images below.

Screen Shot 2022-07-27 at 22.10.05.png

Screen Shot 2022-07-27 at 22.11.01.png

Screen Shot 2022-07-27 at 22.11.50.png

Screen Shot 2022-07-27 at 22.12.53.png

After a new project is created and successfully build, Hello Android is shown in the above image in the preview panel.

Let's break down the initial project and try to understand a few basics.

Project Structure

The generated android project has the following folder structure and the application starts with MainActivity along with theme-related files like Color.kt, Theme.kt, Type.kt.

Composable Functions

Composable functions must be annotated with @Composable at the top. These functions do not have a return type but can have multiple function parameters. The content inside the composable function is rendered to make the application layout design.

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

Preview

The design preview is generated by the Android Studio if any composable function is further annotated with @Preview.

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    FirstComposeAppTheme {
        Greeting("Android")
    }
}

image.png

If showSystemUI is set to true in Preview. The design preview is shown in Android Device Frame.

@Preview(showBackground = true, showSystemUi = true)
@Composable
fun DefaultPreview() {
    FirstComposeAppTheme {
        Greeting("Android")
    }
}

image.png

MainActivity.kt

This file (activity class) is the start point for working with Compose UI. This activity class must also be defined inside the app/src/main/AndroidManifest.xml as launcher activity. Compose codes must be written inside setContent dsl function.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            FirstComposeAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

Theme.kt

This file is used to define color schemes and themes for the composable functions (UI components) used inside the application. Here, a composable function is defined to provide the themed wrapper to the child composables. This wrapper helps to inherit the defined theme to the child compose UI components.

Screen Shot 2022-07-28 at 21.58.43.png

@Composable
fun FirstComposeAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

Basic Composable Components

Text

High level element that displays text and provides semantics / accessibility information.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            FirstComposeAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    LayoutBasics()
                }
            }
        }
    }
}

@Composable
fun LayoutBasics() {
    Text(text = "Hello Jetpack!")
}

@Preview(showBackground = true, showSystemUi = false)
@Composable
fun DefaultPreview() {
    FirstComposeAppTheme {
        LayoutBasics()
    }
}

Here, LayoutBasics is a composable function that contains another composable function Text and renders the text Hello Jetpack! as in image below.

image.png

image.png

The Text function has various parameters to define the color, font size, font type, font style, text alignment, etc. Here is the example of a text with two lines, overflow ellipsis and other various stylings.

@Composable
fun LayoutBasics() {
    val longText =
        "Hello Jetpack!\nThe quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog"
    Text(
        text = longText,
        modifier = Modifier.padding(16.dp),
        color = Color.Red,
        fontSize = 20.sp,
        fontStyle = FontStyle.Italic,
        letterSpacing = 5.sp,
        textDecoration = TextDecoration.Underline,
        textAlign = TextAlign.Start,
        overflow = TextOverflow.Ellipsis,
        softWrap = true,
        maxLines = 2,
        style = TextStyle.Default,
    )
}

@Preview(showBackground = true, showSystemUi = false)
@Composable
fun DefaultPreview() {
    FirstComposeAppTheme {
        LayoutBasics()
    }
}

image.png

The detailed explanation on Text and TextField can be found here.

Image

Creates a composable that lays out and draws a given source like painter. The drawable image resource is passed inside the painter resource and is used as a parameter for Image composable.

@Composable
fun LayoutBasics() {
    // image path: app/src/main/res/drawable/jetpack_compose.png
    Image(
        painter = painterResource(R.drawable.jetpack_compose),
        contentDescription = null,
        modifier = Modifier
            .padding(16.dp)
            .size(100.dp)
            .border(1.dp, MaterialTheme.colorScheme.secondary, RoundedCornerShape(8.dp))
    )
}

@Preview(showBackground = true, showSystemUi = false)
@Composable
fun DefaultPreview() {
    FirstComposeAppTheme {
        LayoutBasics()
    }
}

image.png

Vector Images can also be rendered like in example below.

@Composable
fun LayoutBasics() {
    Image(
        imageVector = Icons.Filled.AccountCircle,
        contentDescription = null, // decorative
        modifier = Modifier.size(40.dp),
        colorFilter = ColorFilter.tint(color = Color.LightGray),
        contentScale = ContentScale.Fit
    )
}

image.png

Row

A layout composable that places its children in a horizontal sequence.

A Row composable used to place components in a horizontal manner side by side. Here is a example of Image icons in a row.

@Composable
fun LayoutBasics() {
   Row {
       ImageIcon()
       ImageIcon()
       ImageIcon()
   }
}

@Composable
fun ImageIcon(){
    Image(
        imageVector = Icons.Filled.AccountCircle,
        contentDescription = null, // decorative
        modifier = Modifier.size(40.dp),
        colorFilter = ColorFilter.tint(color = Color.Gray),
        contentScale = ContentScale.Fit
    )
}

@Preview(showBackground = true, showSystemUi = false)
@Composable
fun DefaultPreview() {
    FirstComposeAppTheme {
        LayoutBasics()
    }
}

image.png

Column

A layout composable that places its children in a vertical sequence.

A Column composable to used to place components in a verticle manner one after another. Here is a example of texts in a column.

@Composable
fun LayoutBasics() {
   Column {
       Text("First Text")
       Text("Second Text")
       Text("Third Text")
       Text("Fourth Text")
   }
}

@Preview(showBackground = true, showSystemUi = false)
@Composable
fun DefaultPreview() {
    FirstComposeAppTheme {
        LayoutBasics()
    }
}

image.png

Combination of Row and Column

Row and Column can be combined to represent UI components. Let's look at the following example. The column contains an Image and Row. The inner row contains an image and another column.

@Composable
fun LayoutBasics() {
    Column(
        Modifier
            .background(Color.LightGray)
            .padding(16.dp)
    ) {

        Image(
            painter = painterResource(R.drawable.begin_cup),
            contentDescription = null,
            modifier = Modifier.clip(RoundedCornerShape(8.dp)),
            contentScale = ContentScale.Fit
        )

        Row(
            modifier = Modifier
                .background(Color.White)
                .fillMaxWidth()
                .padding(PaddingValues(16.dp))
        ) {
            Image(
                imageVector = Icons.Default.AccountCircle,
                modifier = Modifier
                    .size(40.dp)
                    .clip(CircleShape)
                    .background(Color.LightGray),
                contentDescription = null,
                contentScale = ContentScale.Fit,
                colorFilter = ColorFilter.tint(color = Color.Green)
            )

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

            Column {
                Text(text = "Begin Cup", fontWeight = FontWeight.Bold)
                Text(text = "A good morning coffee")
            }
        }
    }
}

@Preview(showBackground = true, showSystemUi = true)
@Composable
fun DefaultPreview() {
    FirstComposeAppTheme {
        LayoutBasics()
    }
}

image.png

In this article, I covered some basics of Jetpack Compose. In the future, I will cover more advanced topics. Stay tuned!

References