Leave a rating/review
Notes: 07. Build Common UI Components - Part 2
The student materials have been reviewed and are updated as of September 2022.
Demo
Next thing to build is the RatingBar. There currently isn’t a built-in control for rating bars in Compose, but it’s very easy to build them yourself!
Create a new file named RatingBar in the composeUi package, and start off with the following code:
@Composable
fun RatingBar(
modifier: Modifier,
range: IntRange,
isLargeRating: Boolean = true,
isSelectable: Boolean = true,
currentRating: Int = 0,
onRatingChanged: (Int) -> Unit = {}
) {
}
Your rating bar will need a range for the number of stars, the currently selected rating, if it’s selectable or not, if it’s a large rating bar, or a small one, and a rating changed handler function.
You’ll use rating bars on list items, as a small rating bar, and in rating pickers for reviewing books, as big rating bars. You also won’t be able to select the rating sometimes, like in list items, or the details screen.
Now to build a rating bar, you will use a List of items, where each item is represented by a star. Jetpack Compose has two specific components to build lists, and they are very easy to use.
To build a horizontal list, start by adding the following code:
val selectedRating = remember { mutableStateOf(currentRating) }
LazyRow(modifier = modifier) {
}
The code represents the currently selected rating state, so you can update the UI once that changes, and a LazyRow component.
There are two main Lazy components in Compose. The LazyRow component builds a horizontal list and smart and lazy item generation to add each item in your data set. Its counterpart, the LazyColumn component, is used to build a vertical list instead.
They are both scrollable too, so you don’t have to worry about that either!
Now add the following code, to represent each item in the list:
items(range.toList()) { index ->
RatingItem(
isSelected = index <= selectedRating.value,
isSelectable = isSelectable, index, isLargeRating
) { newRating ->
selectedRating.value = newRating
onRatingChanged(selectedRating.value)
}
}
@Composable
fun RatingItem(
isSelected: Boolean,
isSelectable: Boolean,
rating: Int,
isLargeRating: Boolean,
onRatingSelected: (Int) -> Unit
) {}
Using items(), you tell the LazyRow to add a list of items, that you’ll display using a collection of data - in your case the range, converted to a list.
You also have functions such as item() and stickyHeader() if you want to add different parts of items to the list.
Each RatingItem will get three boolean flags to change its behavior. One dictates if it’s selected or not, one if it’s selectable, and one if it’s supposed to be a big star or a small one. It also receives the rating it represents and a handler if you select this item.
Next, add the following code, to start building the state and handlers for the RatingItem:
val padding = if (isLargeRating) 2.dp else 0.dp
val size = if (isLargeRating) 32.dp else 16.dp
val baseModifier = if (isSelectable) {
Modifier.clickable(onClick = { onRatingSelected(rating) }, indication = null)
} else {
Modifier
}
Depending on if the star should be big or small, you change their size and padding.
And the base modifier defines a clickable modifier if you can select the item, or an empty modifier if you can’t.
Finally, add the actual UI component:
Icon(
tint = colorResource(id = R.color.orange_200),
modifier =
baseModifier
.size(size)
.padding(padding),
imageVector = if (isSelected) Icons.Default.Star else Icons.Default.StarOutline,
contentDescription = rating.toString()
)
Here you’re building a simple icon which can be clicked, because of the clickable() modifier you used. You also tweak its size and padding accordingly. Finally, you either show the default star, or an empty one, if the item is selected or not and you add a content description.
Go back to the BookFilter.kt file and import this RatingBar, like so:
RatingBar(
modifier = Modifier.align(CenterHorizontally), // new code + import
range = 1..5,
currentRating = currentRatingFilter.value,
isLargeRating = true,
onRatingChanged = { newRating -> currentRatingFilter.value = newRating })
And make the filter full-screen, by changing its Column modifier to modifier.fillMaxSize().
Now, add a file to the ui package in books, named BooksList, and add the following empty function:
@Composable
@Preview
fun BooksList(
books: List<BookAndGenre> = emptyList()
) {
}
This composable will represent the list of books in the BooksFragment, but you’ll build that in the second part of this episode! Go back to BooksFragment.kt and import the BooksList(), to be able to build the project.
For now, build and run the app after checking that all the imports are fine, and play with the filter drawer you just built.
[Build & Run, play with Filter]
Everything works fine! What’s happening is that the BookFilter’s state changes and updates the UI. Any time you change the currentlySelectedFilter, the UI notices the change and recomposes itself, showing new options in the filter.
Then when you select or prefill any data and choose the filter, you propagate the value back to the BooksFragment, and the data is filtered! You can now filter the data without any trouble, in a nice and stylish way!
But there still is no data to filter - there are no books in the UI. :/
Now that you’ve built the filter, you can proceed to add the BooksList component!
Start off by opening the BooksList file and adding the following code:
@Composable
@Preview
fun BooksList(
books: List<BookAndGenre> = emptyList()
) {
LazyColumn(modifier = Modifier.padding(top = 16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp)) { // here
items(books) { bookAndGenre ->
BookListItem(bookAndGenre)
}
}
}
Just like with the LazyRow, you are building a list of items using the LazyColumn. The difference is that you’re building a vertical list instead.
Another component you’re using here is a VerticalArrangement. spacedBy() is Jetpack Compose’s way to add spacing to list items. Instead of adding spacing between items, you can also use Spacers, which are just views adding space.
This spacing is 2dp in size, so it’ll effectively add a bottom margin of 2dp to each item.
Now proceed to build the BookListItem:
@Composable
fun BookListItem(
bookAndGenre: BookAndGenre
) {
Card(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
elevation = 8.dp,
border = BorderStroke(1.dp, MaterialTheme.colors.primary),
shape = RoundedCornerShape(16.dp)
) {}
}
This is another new component you haven’t used before - a Card! This represents the CardView from the Android toolkit; a view which has an elevation, and a rounded corner shape most of the time.
You added lots of styling here, most of which is using modifiers. Using the wrapContentHeight() modifier, you tell the item to wrap the height. Using fillMaxWidth() you make the item match the parent’s width, and with paddings, you add extra empty space to the component.
Then you added an elevation to the item, and a RoundedCornerShape. The shape is used to draw rounded corners, and you can define the corners in a percentage, in dp size or in pixels.
Finally, you added a border to the component, by using the BorderStroke function, to add a 1dp stroke. All of this will help you style your card, and make it a bit nicer! :]
Now add the following code, to describe its contents:
Row(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.width(16.dp))
Column {
Text(
modifier = Modifier.padding(top = 16.dp),
text = bookAndGenre.book.name,
color = MaterialTheme.colors.primary,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(2.dp))
}
}
So far you’ve built a Card with a Row and added the first text element - the Book name. Now add the rest of the elements.
Text(
fontSize = 16.sp,
text = bookAndGenre.genre.name,
fontStyle = FontStyle.Italic
)
Spacer(modifier = Modifier.height(8.dp))
Text(
fontSize = 12.sp,
overflow = TextOverflow.Ellipsis,
text = bookAndGenre.book.description,
fontStyle = FontStyle.Italic,
modifier = Modifier.fillMaxHeight().padding(end = 16.dp)
)
Spacer(modifier = Modifier.height(16.dp))
You define a Row, to represent a horizontal arrangement of two items, a Spacer which will add margins to the contents, and a Column, which will hold the data.
Within the column, you define three texts and spacers in between them. The texts represent the title or name of the book, the genre name, and the description, respectively.
You also added more styling to the text, by using fontWeight, fontStyle, fontSize and overflow properties of the Text component. The weight defines if the text is bold, extra, semi bold, or black in weights, the style if it is normal or italics, and the fontSize defines the size of the text.
And finally, the overflow defines an ellipsis at the end, in case the text is too long.
Let’s also change the LiveData to mutableState in the BooksFragment:
private val _booksState = mutableStateOf(emptyList<BookAndGenre>())
private val _genresState = mutableStateOf<List<Genre>>(emptyList())
Now that you’ve defined the BooksList component, run the app again, and check out your books!
[Build & Run]
The book items look so good! And you can also filter them out! :]
Note that this is test data, but you can put in real books in here!
However, there’s a small bug, where you can’t scroll your list, because the bottom drawer eats the scroll gesture.
This is being fixed by the Compose team, so stay tuned for updates!
Now you can finally proceed to build the BookReviews feature, which you’ll do in the final step of this three-part-chunk! See you there! :]