Leave a rating/review
Notes: 13. Reuse UI in Multiple Screens
The student materials have been reviewed and are updated as of September 2022.
Intro
[Slide 1 - Generics & reusability]
One of the best things about Jetpack Compose is that it’s powered fully by Kotlin. Unlike the previous toolkit, which requires both XML and Kotlin or Java code to work, this means you can write all your data display logic and all your UI code within the same component.
That’s kind of what Compose is about! :] But it’s even better because you can use Kotlin’s strong type system and generic functions, to make components even more reusable! Let’s do that!
Demo
Generics are the best example of reusability when it comes to writing code. Whenever you write a generic function, you can write it to fit many or all types of data you send to it, while keeping the base functionality the same.
When combining this with Compose, this lets you write powerful UI components that fit to your every requirement!
And one of the best examples of generics in your app are the Dropdown menus, so create a new file called SpinnerPicker and add the following code to:
@Composable
fun <T> SpinnerPicker(
pickerText: String,
preselectedItem: T? = null,
items: List<T>,
itemToName: (T) -> String,
onItemPicked: (T) -> Unit
) {}
Every generic function has to define at least one generic parameter type. That’s why it’s a generic function! In this case, the generic type will dictate the preselectedItem, in case you need to select the item up front.
It also dictates the list of items that you want to show and how they are going to be translated into Strings, using an itemToName lambda function.
And finally, you define the onItemPicked lambda function whenever you pick an item.
All of this is generic, so it could show a list of Book, BookReview, Genre or any other type of objects.
Now add the following code to start building the dropdown menu:
val isPickerExpanded = remember { mutableStateOf(false) }
val pickedItem = remember { mutableStateOf(preselectedItem?.let(itemToName) ?: "") }
Row(
modifier = Modifier
.wrapContentWidth()
.padding(start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
DropdownMenuButton(pickerText, onClick = { isPickerExpanded.value = true })
DropdownMenu(
modifier = Modifier.height(300.dp).width(200.dp),
expanded = isPickerExpanded.value,
onDismissRequest = { isPickerExpanded.value = false }) {
}
}
So far you’ve added the state handling and the parent Row.
You then build the Dropdown menu button, to open it and the actual dropdown menu base.
Now add the following code to build the items:
for (item in items) {
DropdownMenuItem(onClick = {
onItemPicked(item)
isPickerExpanded.value = false
pickedItem.value = itemToName(item)
}) {
Text(text = itemToName(item))
}
}
Using a for loop again, you build the list of DropdownMenuItems. Notice how they are fully generic, as you’re using the lambdas for clicks and converting each item to a String, to represent its name.
This way you don’t have to care if you’re showing books or reviews or genres. Now add the last piece of code to represent the label next to the button:
val currentlySelected = if (pickedItem.value.isEmpty()) "None" else pickedItem.value
Text(
text = stringResource(id = R.string.current_selection, currentlySelected),
color = MaterialTheme.colors.onPrimary
)
That’s it! Just by building a small component with a few generics, you can easily reuse this component! Let’s do that with the pickers you used to use.
Now delete the GenrePicker and the BookPicker file. Now open the AddBookActivity and change the code to the following:
private val _genresState = mutableStateOf(emptyList<Genre>())
SpinnerPicker(
pickerText = stringResource(id = R.string.genre_select),
items = genres,
itemToName = { it.name },
onItemPicked = {
_addBookState.value = _addBookState.value.copy(genreId = it.id)
})
Just with a few small changes and you have the exact same behavior, that you can use in multiple screens. Now do the same for the AddBookReviewActivity:
SpinnerPicker(
items = _books.value,
preselectedItem = currentlySelectedBook.value,
onItemPicked = { bookAndGenre ->
_bookReviewState.value = _bookReviewState.value?.copy(bookAndGenre = bookAndGenre)
currentlySelectedBook.value = bookAndGenre
},
itemToName = { it.book.name },
pickerText = stringResource(id = R.string.book_picker_button_text)
)
And finally, the BookFilter:
SpinnerPicker(
pickerText = stringResource(id = R.string.genre_select),
items = genres,
preselectedItem = currentlySelectedGenre,
itemToName = { it.name },
onItemPicked = { currentGenreFilter.value = it })
You can notice how each of the features used the same component that you built and how the usage is more less the same.
You not only saved yourself from having to build multiple different components for each of the objects you can pick, which is a ton of boilerplate code, but also from having to worry about the consistency between these components.
And finally, you don’t have to worry about changing all of the components in case you want to change the design or the functionality. It’s all going to update automatically, with the one generic component.
Just a small fix that I’ve noticed before we build. Now build and run the app, and you should see that everything still works.
[Build & Run]
Awesome! Just by building a simple generic component you can save yourself a lot of time and from writing boilerplate code! :]
Now let’s do the same thing for the DeleteDialog. If you remember, you used two different dialogs to delete items, but this can be fully generic too!
Open the DeleteDialog file, and add the following code:
@Composable
fun <T> DeleteDialog(
item: T,
message: String,
onDeleteItem: (T) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
title = { Text(text = stringResource(id = R.string.delete_title)) },
text = { Text(text = message) },
onDismissRequest = onDismiss,
buttons = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
DialogButton(
text = R.string.yes,
onClickAction = { onDeleteItem(item) }
)
DialogButton(text = R.string.cancel, onClickAction = onDismiss)
}
})
}
I’ve just copied and pasted the code here for ease of use, but the dialog is nearly the same as the other two you’re using, but instead of using different types, you’ve made them generic!
Now delete the other two dialogs.
[Delete dialogs]
It’s time to replace their usage with the now-generic dialog. Open the BookReviewsFragment, and Replace the composable call with the new dialog.
Hey look, it already works out of the box! By just changing the types to be generic, the dialog conforms to the definition, and things just work! Now change the dialog in BooksFragment.
[Replace just the name]
It works out of the box! You can see how there are many places in which you can fall back to generic code, instead of using hardcoded types, when building components with Jetpack Compose.
That’s awesome! Build and run the app, and everything should work fine! :]
[Build & Run the app, delete items]
You’ve almost gone through all the major parts of Jetpack Compose! The last thing you need to go through in the next episode, is using animations to make your apps look awesome! See you there! :]