Chapters

Hide chapters

Jetpack Compose by Tutorials

Second Edition · Android 13 · Kotlin 1.7 · Android Studio Dolphin

Section VI: Appendices

Section 6: 1 chapter
Show chapters Hide chapters

10. Building Complex UI in Jetpack Compose
Written by Prateek Prasad

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

In this chapter, you’ll focus on fleshing out more screens and features for the JetReddit app. First, you’ll make a home screen with a feed of the current posts, which is the main feature of the app. Then, you’ll build a screen where you can see a feed 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 will most certainly have more than two posts, so the whole screen is scrollable. As 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 have these post type variants as separate composables, so the only thing you need to do in code is change between the two types based on the data.

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.

@Composable
fun Header(post: PostModel) {
   Row(modifier = Modifier.padding(start = 16.dp)) {
     Image(
         ImageBitmap.imageResource(id = R.drawable.subreddit_placeholder),
         contentDescription = stringResource(id = R.string.subreddits),
         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)
}
Header Preview
Ceezed Prenauf

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.

@Composable
fun ArrowButton(onClickAction: () -> Unit, arrowResourceId: Int) {
  IconButton(onClick = onClickAction, modifier = Modifier.size(30.dp)) {
    Icon(
      imageVector = ImageVector.vectorResource(arrowResourceId),
      contentDescription = stringResource(id = R.string.upvote),
      modifier = Modifier.size(20.dp),
      tint = Color.Gray
    )
  }
}
Arrow Button Preview
Uwxeg Kijluw Flaboab

@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)
  }
}
Voting Action Button Preview
Bikoyw Utroup Qotjir Syuvoix

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 = {}) {
  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)
    }
  }
}
Post Preview
Semx Pqopeak

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 ?: R.drawable.compose_course)
  }
}
@Preview
@Composable
fun ImagePostPreview() {
  Post(DEFAULT_POST) {
    ImageContent(DEFAULT_POST.image ?: R.drawable.compose_course)
  }
}
Image Post Preview
Uwefu Hans Zxopoih

Adding Multiple Posts

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

@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))
    }
  }
}
Home Screen
Kozo Dyyuiw

Building the Subreddits Screen

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

Subreddit Screen Example
Kewjafqut Vlbiuf Uyodjfi

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.

@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)
      }
    )
  }
}
Subreddit Body Preview
Jimkoxpup Quxr Mregiom

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.

@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)
  }
}
Subreddit Preview
Xuvherzen Nrepoep

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, onCommunityClicked: () -> Unit = {}) {
  Row(modifier = modifier
    .padding(start = 16.dp, top = 16.dp)
    .fillMaxWidth()
    .clickable { onCommunityClicked.invoke() }
  ) {
    Image(
        bitmap = ImageBitmap.imageResource(id = R.drawable.subreddit_placeholder),
        contentDescription = stringResource(id = R.string.community_icon),
        modifier = 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)
    )
  }
}
Community Preview
Postinaxx Dfaheay

Adding a Community List

In this section, you’ll add the a list to show the communities available. 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))
  }
}
Communities Preview
Kihyewipuiv Wtotauw

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.

@Composable
fun SubredditsScreen(modifier: Modifier = Modifier) {
  Column(modifier = modifier.verticalScroll(rememberScrollState())) {
    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)
  }
}
Subreddits Screen
Tirwekjigk Bxmeog

Key Points

  • If you see parts of a screen that repeat, use a component-based approach to extract them into separate composables.
  • Use Preview for iterating on the implementation of the components until you’ve built your whole screen.
  • Use emptyContent() to display empty content inside the composable.
  • Use zIndex() if multiple composables overlap and you want to change their order of display in the z-axis.
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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now