Practical Compose Slot API example
May 31, 2022
Slot API pattern in Compose is a common pattern in Compose that offers “slots”— generic lambdas that accept composable content. There is a great post about it by Chris Banes at https://chris.banes.dev/slotting-in-with-compose-ui/.
Perhaps the most prominent example of this pattern is Scaffold
. Scaffold
provides slots for the most common top-level Material components, such as TopAppBar
, BottomAppBar
, FloatingActionButton
, and Drawer
. Its pluggable nature makes it easy to make sure these components are properly positioned and work together correctly.
@Composable fun HomeScreen(/*...*/) { Scaffold( drawerContent = { /*...*/ }, topBar = { /*...*/ }, content = { /*...*/ } ) }
Building configurable parent layouts using content lambdas allows us to delegate each slot’s content to the caller and tends to offer future reuse and flexibility (it depends on your particular use case whether Slot API is the right choice for your needs).
Screen layout as a set of slots
In this post, we’ll look at another common example: laying out sections of a screen. The sections will have enough in common to use Slot API pattern to model the our HomeScreen
as a set of HomeSection
composables.
You can find the source code at https://github.com/jshvarts/ComposeSlotApiDemo.
Here is the UI we are building:
Chances are you have come across this design pattern before when building screens for Android—a screen is composed of several sections and their layouts follow a similar pattern. For instance, a section can have a title, content, sometimes description and action buttons, or something along those lines.
Here are the 3 sections that can be templated in the screen above:
Top Rated
Action
Animation
Each home section has a title and content and some of the sections have a See All button that lets you go to another screen with content filtered for that particular movie genre.
HomeSection slot
Let’s see how we can build such a Section slot:
@Composable fun HomeSection( @StringRes title: Int, filter: SectionFilter? = null, modifier: Modifier = Modifier, content: @Composable () -> Unit ) { Column { if (filter == null) { SectionTitle(title = title) } else { Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier .fillMaxWidth() ) { SectionTitle( title = title, modifier = modifier .alignByBaseline() ) SectionFilterButton( filterInfo = filterInfo, modifier = Modifier .alignByBaseline() ) } } content() } }
HomeSection
is a slot that can hold a section of the home screen. It is defined as:
mandatory section title
optional filter (a way to build a “See All” button applicable to some sections only)
mandatory content lambda
Notice the composable content
lambda. Slot API pattern takes advantage of the power of Kotlin to build flexible layouts—each section will lay out its data as according to its own unique needs.
Also notice that we modeled the filter info as follows:
data class SectionFilter( @StringRes val text: Int = R.string.section_filter_text_default, val onClick: () -> Unit )
Note that the filter is modeled as a data class rather than separate
text: String?
andonClick: (() -> Unit)?
to avoid getting into an inconsistent state of a nulltext
and non-nullonClick
lambda and vice versa.
If filter
is not applicable for a given section, null is passed in and we just render SectionTitle
. Otherwise, we render a Row
composable containing SectionTitle
and SectionFilterButton
composables.
As noted above, each of the 3 sections has a common overall structure but the UI for section content is slightly different and we allow for future flexibility.
Top Rated content is implemented as a
LazyRow
to display movie posters with items larger than other in other sections.Action content is also implemented as a
LazyRow
with items smaller than those in the Top Rated section.Animation section uses
LazyHorizontalGrid
. I just chose this arbitrary layout to demonstrate the flexibility we get with building sections in this modular way.
To review building lazy layouts with Compose, check out my other posts including Compose Row, Column and Scoped Modifiers, Lazy layouts contentPadding and Lazy Grid layouts in Compose.
HomeScreen layout
Setting up screen sections as slots gives you a blueprint for what this screen is about and makes it easier to work with it. If we have to add another section in the future, this code structure should make it pretty easy to do by adding an additional HomeSection
block.
Here is our HomeScreen
layout containing the screen title and utilizing the section slots defined above:
@Composable fun HomeScreen( navController: NavController, modifier: Modifier = Modifier, homeViewModel: HomeViewModel = hiltViewModel() ) { val state by homeViewModel.uiState.collectAsState() if (state.isLoading) { LoadingIndicator() } else { Column( modifier .verticalScroll( rememberScrollState() ) ) { Spacer(Modifier.height(16.dp)) ScreenTitle(R.string.screen_title_home) HomeSection(title = R.string.section_title_top_rated) { TopRatedMovieList(state.topRatedMovies) } HomeSection( title = R.string.section_title_action, filter = SectionFilter { navController.navigate(Screen.ActionMovies.route) } ) { ActionMovieList(state.actionMovies) } HomeSection( title = R.string.section_title_animation, filter = SectionFilter { navController.navigate(Screen.AnimationMovies.route) } ) { AnimationMovieList(state.animationMovies) } Spacer(Modifier.height(16.dp)) } } }
Just by looking at the HomeScreen
UI tree, it’s clear what sections it consists of and which components comprise the sections:
ScreenTitle
HomeSection
for Top Rated (no filter button). Delegates content building toTopRatedMovieList
HomeSection
for Action movies (with filter button). Delegates content building toActionMovieList
HomeSection
for Animation movies (with filter button). Delegates content building toAnimationMovieList
Delegating implementation detail to a component makes the code more self-documenting and adhere to single responsibility principles.
Also notice that we utilize state hoisting pattern to pass a list of movies to each section stateless component.
TopRatedMovieList
@Composable fun TopRatedMovieList(movies: List<Movie>) { LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues( start = 16.dp, end = 16.dp ) ) { items(movies) { movie -> HomePosterImage(movie) } } }
ActionMovieList
@Composable fun ActionMovieList(movies: List<Movie>) { LazyRow( modifier = Modifier .height(160.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues( start = 16.dp, end = 16.dp ) ) { items(movies) { movie -> HomePosterImage(movie) } } }
AnimationMovieList
@Composable fun AnimationMovieList(movies: List<Movie>) { LazyHorizontalGrid( rows = GridCells.Fixed(2), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues( start = 16.dp, end = 16.dp ), modifier = Modifier .height(340.dp) ) { items(movies) { movie -> HomePosterImage(movie) } } }
Genre-filtered screens
Tapping on the See All filter button for a specific genre, opens a new screen that displays movies for that genre only. Here is an example of filtering Action movies only:
Again, full source code for this post at https://github.com/jshvarts/ComposeSlotApiDemo. Pagination was out of scope for this post but the code sample includes a good example of how some of these tie together:
Jetpack Compose
Jetpack Compose Navigation
Hilt for Dependency Injection
ViewModel using Coroutines and StateFlow
Retrofit with OkHttp and Moshi