Making Complex UIs using Jetpack Compose. Building Leadership App

Making Complex UIs using Jetpack Compose. Building Leadership App

I have recently started to learn Jetpack Compose, and honestly, I don't know why it took me so long because it's amazing and I have had a lot of fun coding in it. If you are looking to get started also I hope this article helps or you learn a thing or two.

Let me stop talking about my romance with Jetpack Compose and start the project.

via GIPHY

What were a going to build

We are going to build the Main Screen for a leadership app. I got the inspiration on dribbble.com from Sulton Handaya, please check out their work here's the link to the shot. Link to the shot dribbble.com/shots/16907854-Leadership-Lear...

Here's a screenshot of the final app.

Screenshot_20211205_135058.png

The final code is in Github github.com/fatahrez/MentorApp. Check out and If you like it give it a star 🙏

Start a new Compose project

To Start a Jetpack Compose project Go to Android Studio > File > New Project.... Then choose Empty Compose activity Screenshot 2021-12-05 at 13.45.54.png

Go ahead and give your app a name and package name.

Screenshot 2021-12-05 at 13.53.54.png

Make sure the language is Kotlin and the Minimum SDK is API 21. Just like in the photo above.

Fundamentals

Let's understand a few things first about Compose before getting started with the code.

  • Jetpack Compose does not use XML layouts unlike the previous system of UI

via GIPHY

what I like about this is that I no longer have to create custom drawable files for complex UIs which was like a lot of work. Also, I like that I do not have to switch between XML and Kotlin code like a hundred times to confirm different things. I think this helps my productivity as a software developer.

  • Compose uses Composables to create blocks of UI elements Let's say you want to create Buttons on the same line this you will use a Composable because they are related. Composable looks something like this
@Composable
fun ContentSection(
    contents: List<Content>
) {
    Row (
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.fillMaxWidth()
    ){
        Text(
            text = "Latest Content",
            style = MaterialTheme.typography.subtitle1
        )

        Box(modifier = Modifier.size(32.dp)) {
            val morePainter = painterResource(id = R.drawable.more)
            Icon(painter = morePainter, contentDescription = "more content")
        }
    }
    Spacer(modifier = Modifier.padding(4.dp))
    LazyColumn() {
        items(contents.size) {
            if (it == 0) {
                Spacer(modifier = Modifier.padding(top = 30.dp))
            }
            Log.i(TAG, "ContentSection: ")
            ContentBox(content = contents[it])
        }
    }
}

This for example is for the content section of an app. To display the content in a list. Ideally, in a Compose project, you will have a bunch of this.

If this sounds complex or greek don't worry I will explain in a few.

Coding the App

Now that you have your project set up and you know a little bit about Compose. Let's code our UI.

Start by creating a new Kotlin file and call it HomeScreen.kt

Top Menu Section

We want to create this part of the UI

Screenshot 2021-12-05 at 14.14.06.png

We will start with defining our @Composable function. Our Code for the TopMenuSection will look like this.

@Composable
fun TopMenuSection() {
    Row (
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier
            .fillMaxWidth()
            .padding(top = 8.dp)
    ){
        Box (
            modifier = Modifier
                .size(45.dp)
                .clip(RoundedCornerShape(50))
                .background(ButtonBlue)
                .padding(8.dp)

        ){
            val painter = painterResource(id = R.drawable.menu)
            Icon(
                painter = painter,
                contentDescription = "menu"
            )
        }
        Text(
            text = "Home",
            style = MaterialTheme.typography.h2
        )
        Row (
            horizontalArrangement = Arrangement.End,
            verticalAlignment = Alignment.CenterVertically
        ){
            val bellPainter = painterResource(id = R.drawable.bell)
            val profilePainter = painterResource(id = R.drawable.woman)
            Box(
                modifier = Modifier
                    .size(25.dp)
            ) {
                Icon(painter = bellPainter, contentDescription = "Notifications")
            }
            Spacer(modifier = Modifier.padding(4.dp))
            Box(
                modifier = Modifier
                    .size(45.dp)
                    .clip(RoundedCornerShape(50))
            ) {
                Image(
                    painter = profilePainter,
                    contentDescription = "Profile",
                    Modifier
                        .background(SecondaryGreen)
                        .padding(4.dp)
                )
            }
        }
    }
}

P.S Note - For the images and drawable please find them here github.com/fatahrez/MentorApp/tree/master/a.. and here github.com/fatahrez/MentorApp/tree/master/a..

So what we have done here is that we have created a Row since all the items will be in the same straight-line(If were to arrange them vertically we would use Column). Inside the Row, we have started by creating a Box to in which we have put our Icon, we have a modifier with clip function to make it a Circle round it.

To put Text in Compose we use the Text() and simply pass through the text. e.g.

Text("Some text")

The text function also takes different things depending on your typography style. We put a Text for Home in the TopMenuSection

We have finally added another Row to align the last two items at the end which are an Icon for notification and an Image for the profile of the User. Try out the code above to check yourself.

Search Section

Now let's do the search section which looks like this

Screenshot 2021-12-05 at 14.40.01.png

The code for this will look like this

@Composable
fun SearchSection() {
    Column {
        Row {
            Text(
                text = "Hallo",
                style = MaterialTheme.typography.h1,
                modifier = Modifier
                    .drawBehind {
                        val strokeWidth = 4.5.dp.toPx() * density
                        val y = size.height - strokeWidth / 2

                        drawLine(
                            ButtonOrange,
                            Offset(0f, y),
                            Offset(size.width, y),
                            strokeWidth
                        )
                    }
            )
            Text(
                text = " Fatah",
                style = MaterialTheme.typography.h1
            )
        }
        Spacer(modifier = Modifier.padding(8.dp))
        Row {
            Text(
                text = "Learn Leadership Training ",
                style = MaterialTheme.typography.body1
            )
            Text(
                text = "Now",
                style = MaterialTheme.typography.subtitle2,
                modifier = Modifier
                    .drawBehind {
                        drawOval(
                            color = Color.Black
                        )
                    }
                    .padding(1.5.dp)
            )
        }
        Spacer(modifier = Modifier.padding(8.dp))
        Searchbar()
    }

}

Don't worry about the SearchBar error we will make another Composable function for it.

Since we need to draw under the word Hallo we have put the text in a row. We have done something interesting for the word Hallo we decide to use a Modifier and draw a line under it using the drawBehind capability of Modifier. We have also the same for the "Now" text where we have drawn an oval behind it.

SearchBar Section

Now that our UI is coming together, Noicee.

via GIPHY

Let's add the searchbar now

@Composable
fun Searchbar() {
    var textFieldState by remember {
        mutableStateOf("")
    }
    Row {
        Row (
            modifier = Modifier.clip(RoundedCornerShape(12.dp))
        ){
            Button(
                onClick = {},
                shape = RoundedCornerShape(20, topEndPercent = 0, bottomEndPercent = 0, 20),
                border = BorderStroke(0.75.dp, color = Color.Black),
                colors = ButtonDefaults.buttonColors(contentColor = Color.Black, backgroundColor = ButtonGrey),
                modifier = Modifier
                    .size(56.dp)
            ) {
                val searchPainter = painterResource(id = R.drawable.outline_search_24)
                Icon(painter = searchPainter, contentDescription = "search")
            }
            TextField(
                value = textFieldState,
                onValueChange = {
                    textFieldState = it
                },
                label = {
                        Text(text = "Search")
                },
                shape = RoundedCornerShape(0, 20, 20, 0),
                singleLine = true,
                colors = TextFieldDefaults.textFieldColors(
                    backgroundColor = Color.White
                ),
                modifier = Modifier
                    .width(215.dp)
                    .border(0.75.dp, color = Color.Black)
            )
        Spacer(modifier = Modifier.padding(5.dp))

        }

        Button(
            onClick = { /*TODO*/ },
            shape = RoundedCornerShape(20),
            border = BorderStroke(0.75.dp, color = Color.Black),
            colors = ButtonDefaults.buttonColors(contentColor = Color.White, backgroundColor = ButtonBlue),
            modifier = Modifier
                .size(56.dp)
        ) {
            val filterPainter = painterResource(id = R.drawable.outline_filter_list_24)
            Icon(painter = filterPainter, contentDescription = "painter")
        }

    }
}

So what we have done here is put all the elements in a row since they will be in the same line. We draw the Button with an icon to show search then add a TextField to show our searchbar then finally add the filter button on the end.

ContentSection

Here's how our content section will look like Screenshot 2021-12-06 at 13.08.39.png

Let's paste the code and discuss it after

@Composable
fun ContentSection(
    contents: List<Content>
) {
    Row (
        horizontalArrangement = Arrangement.SpaceBetween,
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.fillMaxWidth()
    ){
        Text(
            text = "Latest Content",
            style = MaterialTheme.typography.subtitle1
        )

        Box(modifier = Modifier.size(32.dp)) {
            val morePainter = painterResource(id = R.drawable.more)
            Icon(painter = morePainter, contentDescription = "more content")
        }
    }
    Spacer(modifier = Modifier.padding(4.dp))
    LazyColumn() {
        items(contents.size) {
            if (it == 0) {
                Spacer(modifier = Modifier.padding(top = 30.dp))
            }
            ContentBox(content = contents[it])
        }
    }
}

@Composable
fun ContentBox(
    content: Content
) {
    Column() {
        Card(
            modifier = Modifier
                .fillMaxWidth(),
            shape = RoundedCornerShape(20),
            border = BorderStroke(1.dp, color = Color.Black)

        ) {
            Box(
                modifier = Modifier.fillMaxSize()
            ) {
                Image(
                    painter = rememberImagePainter(
                        data = content.image
                    ),
                    contentDescription = content.title,
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .height(150.dp)
                        .fillMaxWidth()
                )
                Button(
                    onClick = {},
                    modifier = Modifier
                        .height(70.dp)
                        .width(150.dp)
                        .padding(top = 16.dp)
                        .align(Alignment.Center),
                    colors = ButtonDefaults.buttonColors(
                        backgroundColor = content.background,
                        contentColor = Color.Black
                    ),
                    border = BorderStroke(2.dp, Color.Black),
                    shape = RoundedCornerShape(20)
                ) {
                    val painter = painterResource(id = R.drawable.outline_play_circle_filled_24)
                    Icon(painter = painter, contentDescription = "play")
                }
            }
        }

        Card(
            shape = RoundedCornerShape(20),
            border = BorderStroke(1.dp, color = Color.Black),
            elevation = 4.dp,
            modifier = Modifier.offset(y = (-180).dp)
        ) {
            Box(
                modifier = Modifier
                    .size(250.dp, 80.dp)
                    .padding(15.dp)
            ) {
                Column {
                    Text(
                        text = content.title,
                        style = MaterialTheme.typography.subtitle1
                    )
                    Spacer(modifier = Modifier.padding(6.dp))
                    Row {
                        Box(modifier = Modifier
                            .size(20.dp)
                            .clip(RoundedCornerShape(50))
                            .background(content.publisherIconBackground)
                            .padding(3.dp)
                        ) {
                            val publisherIcon = painterResource(id = content.publisherIcon)
                            Image(painter = publisherIcon, contentDescription = content.publisher)
                        }

                        Spacer(modifier = Modifier.padding(4.dp))

                        Text(
                            text = content.publisher,
                            style = MaterialTheme.typography.body1
                        )

                        Spacer(modifier = Modifier.padding(8.dp))

                        Box(modifier = Modifier.size(18.dp)) {
                            val timeIcon = painterResource(id = R.drawable.chronometer)
                            Image(painter = timeIcon, contentDescription = content.time)
                        }

                        Spacer(modifier = Modifier.padding(4.dp))
                        Text(
                            text = content.time,
                            style = MaterialTheme.typography.body1
                        )

                        Spacer(modifier = Modifier.padding(8.dp))
                        Box(modifier = Modifier.size(18.dp)) {
                            val heartIcon = painterResource(id = R.drawable.heart_black)
                            Image(painter = heartIcon, contentDescription = content.time)
                        }
                    }
                }
            }
        }
    }
}

Here we have 2 Composables. The first one ContentSection which takes in a List of our content. For this, we need to create a data class that looks like this.

data class Content (
    val title: String,
    val publisher: String,
    @DrawableRes val publisherIcon: Int,
    val publisherIconBackground: Color,
    val time: String,
    val liked: Boolean,
    val image: String,
    val background: Color
)

We will need this to create a dynamic list of the content, kind of like getting data from API to show different content and views.

For the ContentSection we start with a Row that takes in the title "Latest Content" and an Icon to show more data.

After that, we create a LazyColumn(Which is preferred over Columns when displaying a large list of data because they are more efficient). Inside the LazyColumn we loop through the list of Content while parsing each inside a ContentBox.

Our next Composable is the ContentBox here we have done a bunch of stuff. We have started with the Card that looks second on the eye. Inside the card, we put an Image and pass it in our image using painter for this we require the Coil dependency to add it use app build.gradle.

    implementation("io.coil-kt:coil-compose:1.4.0")

We also add a button and align it in the center.

For the second card in order for it to appear above the first we Modifier.offset(y = (-180).dp). We move its y-axis above so that it shows above the other card. We pass in the text as content.title. Also adding all the icons from using our content data class.

Now we are done with all the sections ...

Bringing it all together

Now we need to content all our sections and oh make our the last 3rd of our screen Green.

Create a new drawable in the same file HomeScreen.kt

@Composable
fun HomeScreen() {
    BoxWithConstraints(
        modifier = Modifier
            .fillMaxSize()
    ) {
        val width = constraints.maxWidth
        val height = constraints.maxHeight

        val greenPoint1 = Offset(width.toFloat(), height*0.58f)
        val greenPoint2 = Offset(-width.toFloat(), height * 0.58f)

        val greenColoredPath = Path().apply {
            moveTo(greenPoint1.x, greenPoint1.y)
            standardQuadFromTo(greenPoint1, greenPoint2)
            lineTo(width.toFloat() + 100f, height.toFloat() + 100f)
            lineTo(-100f, height.toFloat())
            close()
        }

        Canvas(modifier = Modifier.fillMaxSize()) {
            drawRect(
                color = SecondaryGreen,
                topLeft = greenPoint2
            )
        }

        Column (
            modifier = Modifier.padding(16.dp)
        ){
            TopMenuSection()
            Spacer(modifier = Modifier.padding(8.dp))
            SearchSection()
            Spacer(modifier = Modifier.padding(8.dp))
            ContentSection(
                listOf(
                    Content(
                        "Evaluation of your skills",
                        "Alfred Neal",
                        R.drawable.woman,
                        SecondaryGreen,
                        "8 min",
                        false,
                        "https://images.unsplash.com/photo-1600880292203-757bb62b4baf?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2340&q=80",
                        ButtonOrange
                    ),
                    Content(
                        "Track your Progress",
                        "Amstronge",
                        R.drawable.female_doctor,
                        AlternateYellow,
                        "5 min",
                        true,
                        "https://images.unsplash.com/photo-1531482615713-2afd69097998?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2340&q=80",
                        ButtonBlue
                    )
                )
            )
        }
    }
}

We start by placing a BoxWithConstraints (This is a box that is fixed in position kind of like in ConstraintLayout in XML) and we let fillMaxSize using a modifier. The BoxWithConstraints helps us get the height and width easily.

Now we go ahead and specify the last third using Offset points in order to draw a rectangle using the Canvas function which we give the Color Green.

After we have done this We create a Column to pass in all our Sections. In our ContentSection, we pass listOf Content and populate it just like in the code above.

Summary

In my opinion, doing this using XML would be a lot more difficult. And that's why I love Jetpack Compose.

If you need the full code for the project check it out on Github github.com/fatahrez/MentorApp. Be sure to give it a star ⭐️🤩

If this was helpful reach out to me on Twitter. Follow me on Github for more projects like this.