Jetpack Compose

Oct 11 2022 · Kotlin 1.7.10, Android 13, Android Studio Chipmunk

Part 1: Jetpack Compose Basics

08. Build Common UI Components - Part 3

Episode complete

Play next episode

Next
About this episode

Leave a rating/review

See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 07. Build Common UI Components - Part 2 Next episode: 09. Apply Error & Data Handling to the UI

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Notes: 08. Build Common UI Components - Part 3

The student materials have been reviewed and are updated as of September 2022.

In ui/reviews/ui/BookReviewsList file, within BookReviewItem() composable function, replace CoilImage() composable by AsyncImage() composable with updated parameters. CoilImage composable is old and not compatible with current Jetpack Compose version.

Transcript: 08. Build Common UI Components - Part 3

Demo

Last thing that you have to build in this three-part chunk of episodes, is the BookReviews and AddBookReview features.

Start off by creating a new file called BookPicker, in the addReview.ui package, and copying the GenrePicker code there.

[Create file, copy GenrePicker code]

The book picker will also be a dropdown menu picker, but for books. You’ll learn how to make these pickers generic and more reusable later in the course. For now, you’ve changed the code to accommodate books:

@Composable
fun BookPicker(
  books: List<BookAndGenre>,
  selectedBookId: String,
  onItemPicked: (BookAndGenre) -> Unit
) {
  val isPickerOpen = remember { mutableStateOf(false) }
  val selectedBookName =
    books.firstOrNull { it.book.id == selectedBookId }?.book?.name ?: "None"

  Row(verticalAlignment = Alignment.CenterVertically) {
    TextButton(
      onClick = { isPickerOpen.value = true },
      content = { Text(text = stringResource(id = R.string.genre_select)) })

    DropdownMenu(
      expanded = isPickerOpen.value,
      onDismissRequest = { isPickerOpen.value = false }) {

      for (book in books) {
        DropdownMenuItem(onClick = {
          onItemPicked(book)
          isPickerOpen.value = false
        }) {
          Text(text = book.book.name)
        }
      }
    }

    Text(text = selectedBookName)
  }
}

This is pretty much the same as the genre picker, except it’s handling books!

Now open the AddBookReviewActivity class, and change the code as such:

private val _books = mutableStateOf(emptyList<BookAndGenre>())

  override fun onCreate(savedInstanceState: Bundle?) {
	...
    loadBooks()
  }

  private fun loadBooks() {
    lifecycleScope.launch {
      _books.value = repository.getBooks()
    }
  }

Now that you’ve added the state holder for books, add the missing UI components.


  @Composable
  fun AddBookReviewForm() {
	...
    val currentRatingFilter = remember { mutableStateOf(0) }
    val currentlySelectedBook = remember { mutableStateOf(EMPTY_BOOK_AND_GENRE) }
      
    ...
      
    BookPicker(books = _books.value,
      selectedBookId = currentlySelectedBook.value.book.id,
      onItemPicked = { bookAndGenre ->
      _bookReviewState.value = _bookReviewState.value?.copy(bookAndGenre = bookAndGenre)
      currentlySelectedBook.value = bookAndGenre
    })
      
    ...
  }

After adding the book picker, finish up with the RatingBar.

RatingBar(
  modifier = Modifier.align(CenterHorizontally),
  range = 1..5,
  currentRating = currentRatingFilter.value,
  isLargeRating = true,
  onRatingChanged = { newRating -> currentRatingFilter.value = newRating })

Lots of code changes, but nothing really complex. You added books state here, and loaded books from the repository in onCreate(). You then filled in the AddBookReviewForm with missing components - the BookPicker and the RatingBar.

Now build & run the app, and you should be able to add a BookReview!

[Build & Run the app, add book review]

But the review is still missing from the list, because you didn’t build the list items! Let’s do that now! Open the BookReviewsFragment and change the code like so:

val bookReviewsState = mutableStateOf(emptyList<BookReview>())

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  super.onViewCreated(view, savedInstanceState)

  lifecycleScope.launch {
    bookReviewsState.value = repository.getReviews()
  }
}

@Composable
fun BookReviewsContentWrapper() {
  val bookReviews = bookReviewsState.value

  BookReviewsList(bookReviews, onItemClick = ::onItemSelected)
}

Not a lot of change. You just loaded the book reviews from the repository, and used them to show them in the BookReviewsList. You still need to build the BookReviewsList though.

To do that, create a new file called BookReviewsList within the reviews.ui package, and add the following starting code:

@Composable
fun BookReviewsList(
  bookReviews: List<BookReview>,
  onItemClick: (BookReview) -> Unit
) {
  LazyColumn {
    items(bookReviews) { bookReview ->
      BookReviewItem(bookReview = bookReview, onItemClick)
    }
  }
}

Just like before, you’re building a vertical list of items! You can see how it’s very easy to learn Compose components, and use them in different parts of your app. Just by playing around with them once or twice, you get the hang of them! :]

Now you need to build the list item.

[Slide 1 - List item with explanation]

The item will feature a card with two core parts, the text or content section, and the image section. This means you have to use a Row with two elements, a Column with multiple text items, and an Image.

The Column will feature four small text items, and a rating. The RatingBar and theText describing it will be within another Row element. The rest is pretty simple.

Let’s see about building this item! :]

[Switch to demo]

Start off by adding the core code:

Card(
    elevation = 8.dp,
    border = BorderStroke(1.dp, MaterialTheme.colors.primary),
    shape = RoundedCornerShape(16.dp),
    modifier = Modifier
      .wrapContentHeight()
      .padding(16.dp)
  ) {

Now that you’ve defined the basic card, add a clickable modifier and the base components such as the Row and the Column.

Card(
    elevation = 8.dp,
    border = BorderStroke(1.dp, MaterialTheme.colors.primary),
    shape = RoundedCornerShape(16.dp),
    modifier = Modifier
      .wrapContentHeight()
      .padding(16.dp)
      .clickable(
        onClick = { onItemClick(bookReview) },
        indication = null
      )
  ) {
    Row(modifier = Modifier.fillMaxSize()) {
      Spacer(modifier = Modifier.width(16.dp))

      Column(
        modifier = Modifier
          .weight(0.6f)
          .fillMaxHeight()
      ) {}

The card is used here to stick to the design system and make the items pop by adding extra elevation. You also let the card have the clickable() modifier, passing in a lambda function to trigger whenever the user clicks on it, and setting the indication as null.

null indication means there won’t be any dark border or background when you click on the component, to make it a bit nicer.

You then build the main Row and its first child, the Column. You also use the weight() modifier here, to make the width 60 percent of the parent, or 0.6 fractions of the parent.

Now add the second child of the Row, the image, preceded by a small spacer:

Notes

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

      Card(
        modifier = Modifier.weight(0.4f),
        shape = RoundedCornerShape(
          topEnd = 16.dp,
          topStart = 16.dp,
          bottomStart = 0.dp,
          bottomEnd = 16.dp
        ),
        elevation = 16.dp
      ) {
          AsyncImage(
            model = bookReview.review.imageUrl,
            contentScale = ContentScale.FillWidth,
            contentDescription = null
          )
      }

You’re using a card here, because you want the image to pop out of the item in the list, giving off three-dimensional vibes to your items!

You also used a RoundedCornerShape, but instead of having all corners rounded, you left one corner as flat, to make the image look as a small bookmark of sorts.

To load the image, you used the CoilImage component. It’s a special third-party component recommended by Google, for your images. It loads the image using the Coil Kotlin framework for image loading, and it does so by using the data parameter you pass in. You also used the contentScale parameter, to make the image fill its width, and scale accordingly.

Now to add the textual content. Add the following components within the Column:

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

Text(
  text = bookReview.book.name,
  color = MaterialTheme.colors.primary,
  fontSize = 18.sp,
  fontWeight = FontWeight.Bold
)

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

Row {
  Text(
    text = stringResource(id = R.string.rating_text)
  )
...
}

After building the first few components and importing necessary resources, add the rating bar and the remaining textual elements.

  RatingBar(
    modifier = Modifier.align(CenterVertically),
    range = 1..5,
    currentRating = bookReview.review.rating,
    isSelectable = false,
    isLargeRating = false
  )
}

Text(
  text = stringResource(
  id = R.string.number_of_reading_entries,
  bookReview.review.entries.size
))

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

Text(
  text = bookReview.review.notes,
  fontSize = 12.sp,
  modifier = Modifier.fillMaxSize(),
  overflow = TextOverflow.Ellipsis,
  fontStyle = FontStyle.Italic,
  maxLines = 4
)

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

The structure is pretty straightforward. You’re adding four elements, and spacers in between. The elements are the book name, as an 18sp sized text. Notice the .sp function, to convert integers into scaled pixels. It’s followed by a Row with a Text for the rating, and a small rating bar, that can’t be selected.

Then you added a text to represent the number of reading entries within the review, and finally one to represent your notes!

Now you finally have a BookReviewItem that you can display in your app! :]

Build & run the app, and you should see your reviews in the list!

[Build & Run]

The card items are very beautiful, follow the material design guidelines, and have an awesome image shape, that kind of resembles a bookmark! :]

Now you can continue to add more features to your app, in the next episode! See you there! :]