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:

  1. Top Rated

  2. Action

  3. 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:

  1. mandatory section title

  2. optional filter (a way to build a “See All” button applicable to some sections only)

  3. 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? and onClick: (() -> Unit)? to avoid getting into an inconsistent state of a null text and non-null onClick 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.

  1. Top Rated content is implemented as a LazyRow to display movie posters with items larger than other in other sections.

  2. Action content is also implemented as a LazyRow with items smaller than those in the Top Rated section.

  3. 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:

  1. ScreenTitle

  2. HomeSection for Top Rated (no filter button). Delegates content building to TopRatedMovieList

  3. HomeSection for Action movies (with filter button). Delegates content building to ActionMovieList

  4. HomeSection for Animation movies (with filter button). Delegates content building to AnimationMovieList

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

Previous
Previous

Animating visibility vs alpha in Compose

Next
Next

Lazy Grid layouts in Compose