Chapters

Hide chapters

Jetpack Compose by Tutorials

First Edition · Android 11 · Kotlin 1.4 · Android Studio Canary

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

10. Building Complex UI in Jetpack Compose
Written by Tino Balint

Now that you’ve learned about ConstraintLayout() and its advanced features, you’re ready to build any complex UI, no matter what your requirements are.

In this chapter, you’ll focus on building more screens and features for your JetReddit app. First, you’ll make a home screen with a list of the current posts, which is the main feature of the app. Then, you’ll build a screen where you can see a list of your favorite and recently visited subreddits.

Building the home screen

To understand your task, take a look at the following example from the original Reddit app:

Reddit Home Screen
Reddit Home Screen

Here, you see a home screen with two posts. The screen consists of a header, content and post actions. There are two types of content, a text and an image. Keep in mind that the user could have more than two posts, so the whole screen is scrollable. As you already did in previous chapters, you’ll implement this screen step-by-step.

Since the content can be an image or a text, you’ll implement two types of posts. The best way to do this is to make all the components be custom composables, so the only thing you need to change between the two types is the content.

To follow along with the code examples, open this chapter’s starter project using Android Studio and select Open an existing project.

Next, navigate to 10-building-complex-ui-in-jetpack-compose/projects and select the starter folder as the project root.

Once the project opens, let it build and sync and you’re ready to go!

You might already be familiar with the project hierarchy from the previous chapter, but in case you aren’t, check out this image:

Project Hierarchy
Project Hierarchy

There are several packages here, but you’ll only change the code within screens, to implement new features of the app, and components for custom composables — for example, Post(), which those screens need.

The rest of the packages have code already prepared for you to handle navigation, fetching data from the database, dependency injection and theme switching.

Once you’re familiar with the file organization, build and run the app. You’ll see a screen like this:

Starting Screen
Starting Screen

It’s an empty home screen. It only contains the app drawer from the previous chapter.

You’re ready to go now. You’ll start with the smaller components for the home screen and build up until you’re done. Your first task is to implement the post’s header.

Adding a post header

Each post on the home screen has a header that contains the following information: the subreddit it belongs to, the name of the user who posted it, how old the post is and its title.

For your first step, open Post.kt inside components and replace Header() with the following implementation:

@Composable
fun Header(post: PostModel) {
   Row(modifier = Modifier.padding(start = 16.dp)) {
     Image(
         imageResource(id = R.drawable.subreddit_placeholder),
         Modifier.size(40.dp)
             .clip(CircleShape)
     )
     Spacer(modifier = Modifier.width(8.dp))
     Column(modifier = Modifier.weight(1f)) {
       Text(
           text = stringResource(R.string.subreddit_header, post.subreddit),
           fontWeight = FontWeight.Medium,
           color = MaterialTheme.colors.primaryVariant
       )
       Text(
           text = stringResource(R.string.post_header, post.username, post.postedTime),
           color = Color.Gray
       )
     }
     MoreActionsMenu()
   }

   Title(text = post.title)
}

Here’s what you did with this code:

  • First, you added a Row() where you placed the icon.
  • Next, you added a Column() to position the two texts one below the other.
  • At the end of the Row(), you added a MoreActionsMenu button, which was already prepared for you.
  • Finally, you placed Title() outside the Row(). Title() is a Text() with custom styling already done for you.

To see your new header, build the project and open the split view to see the previews. Once the build finishes, look at the preview called HeaderPreview:

Header Preview
Header Preview

The header now has all the the elements it needs to have. Don’t worry that the colors don’t match your design, they’ll change to fit the theme when you run the app.

The next component you’ll write is the voting action button.

Building the voting action button

The voting action button has two images and a text, which makes it slightly different from other action buttons. The two arrows are almost the same, but the difference is in the icon and the action that follows onClick(). Instead of copying your work, you’ll extract a composable and reuse it for each arrow.

Replace ArrowButton() code with the following:

@Composable
fun ArrowButton(onClickAction: () -> Unit, arrowResourceId: Int) {
  IconButton(onClick = onClickAction, modifier = Modifier.size(30.dp)) {
    Icon(
      vectorResource(arrowResourceId),
      modifier = Modifier.size(20.dp),
      tint = Color.Gray
    )
  }
}

Here, you added IconButton() with a modified color and size. You pass onClick() and the vector resource as parameters because you want to reuse this composable for both the up and down arrows.

To see your arrow button, build the project and look at the preview screen under ArrowButtonPreview():

Arrow Button Preview
Arrow Button Preview

You see a simple up arrow. Now, you’ll use this composable to complete the voting action button.

Replace VotingAction() code with:

@Composable
fun VotingAction(
  text: String,
  onUpVoteAction: () -> Unit,
  onDownVoteAction: () -> Unit
) {
  Row(verticalAlignment = Alignment.CenterVertically) {
    ArrowButton(onUpVoteAction, R.drawable.ic_baseline_arrow_upward_24)
    Text(
      text = text,
      color = Color.Gray,
      fontWeight = FontWeight.Medium,
      fontSize = 12.sp
    )
    ArrowButton(onDownVoteAction, R.drawable.ic_baseline_arrow_downward_24)
  }
}

You added a Row() with two ArrowButtons and a Text() in between. For each ArrowButton() you passed a different onClick() and vector drawable. That lets you set a different arrow image and the handler that defines what happens after clicking the button.

Build the project and look at the preview section under VotingActionPreview():

Voting Action Button Preview
Voting Action Button Preview

You now see the two arrows, one for up-voting and one for down-voting. In the middle, you see the total number of votes.

The actions for commenting, sharing and awarding are very similar so they’re pre-made for you. If you’re interested in how they work, look at PostAction() in the starter project.

The last thing that’s missing to complete Post() are its two content types. This time, you’ll use a different approach: building the Post() before you finish the content.

Building the post

You might wonder how you’ll build Post() without first implementing the content. To find out how — and why — make the following changes to Post():

@Composable
fun Post(post: PostModel, content: @Composable () -> Unit = emptyContent()) {
 Card(shape = MaterialTheme.shapes.large) {
   Column(modifier = Modifier.padding(
       top = 8.dp,
       bottom = 8.dp)
   ) {
     Header(post)
     Spacer(modifier = Modifier.height(4.dp))
     content.invoke()
     Spacer(modifier = Modifier.height(8.dp))
     PostActions(post)
   }
 }
}

First, you added a Card() and a Column() to lay out the composable. Then, you added Header(), content and PostActions().

It looks like you call content here even though you haven’t implemented it yet. That’s because, this time, you’re using a composable as a function parameter, which you then invoke precisely where you need it.

content is present, but it’s empty by default unless you provide it. You achieve this by calling emptyContent(), which returns — as its name says — empty content for the composable function.

Build the app and look at PostPreview() under the preview section:

Post Preview
Post Preview

As you see, the post already contains the header and the actions. The only thing missing is the content. You’ll address that part now.

Adding the content

Look at TextPost() and ImagePost():

@Composable
fun TextPost(post: PostModel) {
 Post(post) {
   TextContent(post.text)
 }
}

@Composable
fun ImagePost(post: PostModel) {
 Post(post) {
   ImageContent(post.image)
 }
}

The functions are already built for you because they only call Post() and pass one parameter: either TextContent() or ImageContent(), depending on what type of content you need to display. Feel free to check them out if you’re curious about their implementation. :]

Next, add the following code to see the preview of a post with an image:

@Preview
@Composable
fun ImagePostPreview() {
  Post(DEFAULT_POST) {
    ImageContent(DEFAULT_POST.image)
  }
}

Build the app and take a look at the preview section under ImagePostPreview():

Image Post Preview
Image Post Preview

Your Post() is now complete with all its content and you’re ready to finish the home screen.

Adding multiple posts

To finish the home screen, you need to add the ability to display multiple posts using Post(), which you just made. The posts should vary by type and content.

In this project, the database, repository and viewmodel layers are already prepared for you because you already covered them in Chapter 7, “Managing State in Compose”.

Your task is to fetch the post data using the prepared classes and then render the content inside HomeScreen().

To start, replace HomeScreen() code with the following:

@Composable
fun HomeScreen(viewModel: MainViewModel) {
  val posts: List<PostModel> by viewModel.allPosts.observeAsState(listOf())

  LazyColumn(modifier = Modifier.background(color = MaterialTheme.colors.secondary)) {
    items(posts) {
      if (it.type == PostType.TEXT) {
        TextPost(it)
      } else {
        ImagePost(it)
      }
      Spacer(modifier = Modifier.height(6.dp))
    }
  }
}

To complete this screen, you did the following:

  • First, you fetched all the posts from the database, which are observed as a state to handle recomposition.
  • Next, you added a LazyColumn() to make a scrollable list of the fetched posts.
  • Finally, you rendered the post depending on its type and put a Spacer() at the bottom to separate the items, using items() from the LazyColumn’.

Build and run the app, then take a look at the main screen when the app opens:

Home Screen
Home Screen

You now see a list of multiple posts with different content, which you can scroll through.

Now that you’ve finished the home screen, your next task is to make the subreddits screen.

Building the subreddits screen

First, take a look at the image below to understand what you’ll build:

Subreddit Screen Example
Subreddit Screen Example

The screen consists of two main parts: a horizontally scrollable list of subreddit items and a vertically scrollable Column() that contains both the subreddit list and a list of communities.

The two items that you’ll build are marked in red. At the top, you see the subreddit body, which holds a subreddit item. Below it, you find the community item, which builds the list of communities.

Building the subreddit body

Look at the subreddit body from the example image once more. It consists of a background image, an icon and three texts. Since some elements overlap, you’ll use ConstraintLayout() for flexibility.

Open screens/SubredditsScreen.kt and replace the code inside SubredditBody with the following:

@Composable
fun SubredditBody(subredditModel: SubredditModel, modifier: Modifier = Modifier) {
  ConstraintLayout(
    modifier = modifier.fillMaxSize().background(color = MaterialTheme.colors.surface)
  ) {
    val (backgroundImage, icon, name, members, description) = createRefs() // 1

    SubredditImage( // 2
      modifier = modifier.constrainAs(backgroundImage) {
        centerHorizontallyTo(parent)
        top.linkTo(parent.top)
      }
    )

    SubredditIcon( // 3
      modifier = modifier.constrainAs(icon) {
        top.linkTo(backgroundImage.bottom)
        bottom.linkTo(backgroundImage.bottom)
        centerHorizontallyTo(parent)
      }.zIndex(1f)
    )

    SubredditName( // 4
      nameStringRes = subredditModel.nameStringRes,
      modifier = modifier.constrainAs(name) {
        top.linkTo(icon.bottom)
        centerHorizontallyTo(parent)
      }
    )

    SubredditMembers( // 5
      membersStringRes = subredditModel.membersStringRes,
      modifier = modifier.constrainAs(members) {
        top.linkTo(name.bottom)
        centerHorizontallyTo(parent)
      }
    )

    SubredditDescription( // 6
      descriptionStringRes = subredditModel.descriptionStringRes,
      modifier = modifier.constrainAs(description) {
        top.linkTo(members.bottom)
        centerHorizontallyTo(parent)
      }
    )
  }
}

There is a lot happening here, so here’s a breakdown, one element at a time:

  1. You first create necessary constraint references using createRefs(). They represent the five elements you’ll show in a subreddit.

  2. The first element is the image, which is centered horizontally and linked to the top of the parent. SubredditImage() is already built for you. Check out its definition to learn more.

  3. Centered horizontally within the parent and vertically within the backgroundImage lies the SubredditIcon(). You also used zIndex() to raise the icon above the backgroundImage. In Jetpack Compose, the composables’ order doesn’t determine their order on the z axis — that depends on the composable’s render speed, instead. That means that SubredditImage() might sometimes appear above SubredditIcon().

    To avoid that, you used zIndex(), which lets you change the order to render composables that share the same parent. The greater the zIndex() value is, the later the app will draw the composable. The default value for zIndex() is 0. By setting it to 1, you ensured that the app will always draw SubredditIcon().

  4. You put the SubredditName() at the bottom of the SubredditIcon(), centered horizontally within the parent.

  5. The SubredditMembers() follows the SubredditName().

  6. And finally, the SubredditDescription() follows SubredditMembers().

To see the changes, build the app and look at the preview section under SubredditBodyPreview:

Subreddit Body Preview
Subreddit Body Preview

Adjusting the elements’ height and shadowing

The elements’ positions are correct, but their height is wrong and there are no visible shadows at the edge.

Next, you’ll fix the height and shadow problem. Replace the Subreddit() code with the following:

@Composable
fun Subreddit(subredditModel: SubredditModel, modifier: Modifier = Modifier) {
  Card(
    backgroundColor = MaterialTheme.colors.surface,
    shape = RoundedCornerShape(4.dp),
    modifier = modifier
      .size(120.dp)
      .padding(
        start = 2.dp,
        end = 2.dp,
        top = 4.dp,
        bottom = 4.dp
      )
  ) {
    SubredditBody(subredditModel)
  }
}

This code places SubredditBody() inside a Card() and sets that card’s size, colors and padding.

Build the app and look at the preview section under SubredditPreview():

Subreddit Preview
Subreddit Preview

The size of the composable is now correct and you can see the shadows at the border. Nice job! You’ve just finished another piece of the puzzle.

Next, you’ll build the community item.

Building the community item

The community item is fairly simple; it only has an icon and a text. To build it, change Community() code to:

@Composable
fun Community(text: String, modifier: Modifier = Modifier) {
  Row(modifier = modifier.padding(start = 16.dp, top = 16.dp)) {
    Image(
        imageResource(id = R.drawable.subreddit_placeholder),
        modifier
            .size(24.dp)
            .clip(CircleShape)
    )
    Text(
        fontSize = 10.sp,
        color = MaterialTheme.colors.primaryVariant,
        text = text,
        fontWeight = FontWeight.Bold,
        modifier = modifier
            .padding(start = 16.dp)
            .align(Alignment.CenterVertically)
    )
  }
}

As you see, this implementation is quite simple. It consists of a Row() with an Image() and a Text() after it.

Build the app and look at the preview section under CommunityPreview():

Community Preview
Community Preview

You see an icon and a community text, as you expect.

Adding a community list

Next, you’ll build the list that contains all the main and added communities.

Add the following code inside Communities():

@Composable
fun Communities(modifier: Modifier = Modifier) {
  mainCommunities.forEach {
    Community(text = stringResource(it))
  }

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

  BackgroundText(stringResource(R.string.communities))

  communities.forEach {
    Community(text = stringResource(it))
  }
}

There are two lists of String resources prepared for you: mainComunities and communities. In the first part of the screen, you added the main communities and in the second part, you added the rest.

Both lists are separated with a Spacer() and a BackgroundText(), which is pre-constructed for you. BackgroundText() is a Text() that contains a background color and fills the whole width of the screen.

Build the app and look at the preview section under CommunitiesPreview():

Communities Preview
Communities Preview

You see both lists of communities separated by the content generated by BackgroundText(). The colors will update when you run the app on a device.

Now that you have all the necessary components, you’re ready to finish SubredditsScreen().

Finishing the screen

The last part of the puzzle to build SubredditsScreen() is to combine everything you’ve built so far into a list that the user can scroll horizontally and vertically.

Start by replacing SubredditsScreen with the following:

@Composable
fun SubredditsScreen(modifier: Modifier = Modifier) {
  ScrollableColumn {
    Column {
      Text(
        modifier = modifier.padding(16.dp),
        text = stringResource(R.string.recently_visited_subreddits),
        fontSize = 12.sp,
        style = MaterialTheme.typography.subtitle1
      )

      LazyRow(
        modifier = modifier.padding(end = 16.dp)
      ) {
        items(subreddits) { Subreddit(it) }
      }
      Communities(modifier)
    }
  }
}

With the previous code, you added ScrollableColumn() as the root so the user can scroll vertically.

Next, you added a Column() with a subtitle at the top. To create the list of horizontally scrolled subreddits, you used LazyRow() and passed an already-prepared list of SubredditModels to items().

Finally, you added Communities() to display all the communities.

Build the app and run it on your device this time. Once you install the app, click the middle icon in the bottom bar.

Subreddits Screen
Subreddits Screen

You see the list of subreddits at the top, which the user can scroll horizontally. Below that is the list of communities. The whole screen scrolls vertically — try it out!

Congratulations! You’ve built the main part of the JetReddit app and learned how to build complex elements. You can find the completed code for this chapter in 10-building-complex-ui-in-jetpack-compose/projects/final.

Key points

  • Build your app by first implementing the most basic components.
  • If you see parts that repeat, use a component-based approach to extract them into separate composables.
  • Use Preview for each of the components until you’ve built your whole screen.
  • Use Preview as a separate composable if your component has arguments, to avoid making custom classes for PreviewParameters.
  • Use emptyContent() to display empty content inside the composable.
  • Use zIndex() if multiple composables overlap.

In this chapter, you learned how to make complex UI elements as well as the importance of following the component-based approach to break complex UI into simpler parts. You’re now ready to build any complex UI in your own apps.

In the next chapter, you’ll learn how to react to Compose’s lifecycle and continue building your JetReddit app.

See you there!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.