Lazy Grids for Android TV using Jetpack Compose

For the past few years, we’ve been getting to grips with Jetpack Compose and how we can use it to build apps for Handheld and Wear OS devices. Late last year, Composables built for Android TV apps started to surface as Alpha releases and in this series of blog posts, I want to dive into these composables and learn how they can be used to create apps for Android TV.


Looking to learn Jetpack Compose? Check out my course, Practical Jetpack Compose


When it comes to displaying browsable grids in apps, we currently have a couple of Lazy Grid composables available in Jetpack Compose. However, when it comes to building TV apps, we have some alternative Lazy composables that are designed specifically for use in TV apps. In this blog post, we’ll explore these additional APIs, how we can use them and why they should be used over the traditional Lazy APIs.


Why not sponsor this blog and reach thousands of developers worldwide?


TvLazyVerticalGrid

The TVLazyVertical is the TV equivalent of the LazyVerticalGrid from the Jetpack Compose APIs. This allows us to display vertically scrolling content in a grid format, composing items as they are required.

When it comes to displaying large sets of related content in your TV app, the TVLazyVerticalGrid is going to be your go-to composable. The grid format allows you to display large datasets that are easy to navigate, with the option to apply filtering to reduce the size of data being composed.

The TVLazyVerticalGrid composable has an almost identical API to that of the LazyVerticalGrid composable.

@Composable
fun TvLazyVerticalGrid(
    columns: TvGridCells,
    modifier: Modifier = Modifier,
    state: TvLazyGridState = rememberTvLazyGridState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    verticalArrangement: Arrangement.Vertical =
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    userScrollEnabled: Boolean = true,
    pivotOffsets: PivotOffsets = PivotOffsets(),
    content: TvLazyGridScope.() -> Unit
)

There are a couple of notable differences when it comes to the TvLazyVerticalGrid and LazyVerticalGrid composables.

  • The TvLazyVerticalGrid contains no flingBehaviour argument
    • For the LazyVerticalGrid, this is used to respond to user scroll gestures, which is not an available behaviour for TV devices
  • TvLazyVerticalGrid contains an additional PivotOffsets argument
    • Due to the hardware input used to navigate on Android TV, the TvLazyVerticalGrid automatically shows the next items during scrolling. The PivotOffsets can be used to offset how much of these next items are displayed. This is a new behaviour for the TvLazyVerticalGrid, so is not available for the LazyVerticalGrid composable

Other than these behaviour changes and the under-the-hood impact that they have, the API can be used in an almost identical way. When it comes to composing the TvLazyVerticalGrid, we can use the following code:

TvLazyVerticalGrid(
    columns = TvGridCells.Fixed(6),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp),
    contentPadding = PaddingValues(16.dp)
) {
    items(items) {
        …
    }
}

When it comes to configuring the display of columns inside of the Vertical Grid, there are two options:

  • Fixed Column Size – fix the size of each column, regardless of available space
  • Adaptive Column Size – apply a minimum size for each column, sharing the remaining space between columns

In the case of applying a fixed column, this means that the number of provided columns will always be used for the Grid, regardless of available space.

TvLazyVerticalGrid(
    columns = TvGridCells.Fixed(6),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp),
    contentPadding = PaddingValues(16.dp)
)

Alternatively, we can use an Adaptive approach, providing a minimum size to be used for the items. This means that the provided dp value will always be used when composing the grid. Any remaining space will be distributed evenly between each of the columns.

TvLazyVerticalGrid(
    columns = TvGridCells.Adaptive(120.dp),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp),
    contentPadding = PaddingValues(16.dp)
)

TvLazyHorizontalGrid

The TvLazyHorizontalGrid is the Tv equivalent of the LazyHorizontalGrid from the Jetpack Compose APIs. This works in the same way as the TvLazyVerticalGrid, except that the grid items are composed horizontally instead of vertically.

When it comes to composing the TvLazyHorizontalGrid, we can use the following code:

TvLazyHorizontalGrid(
    Rows = TvGridCells.Fixed(3),
    verticalArrangement = Arrangement.spacedBy(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp),
    contentPadding = PaddingValues(16.dp)
) {
    …
}

Similar to the TvLazyVerticalGrid, we can use either Fixed or Adaptive composition of grid items. When using the Fixed approach, the number of rows will be fixed to the provided number.

TvLazyHorizontalGrid(
        verticalArrangement = Arrangement.Center,
        horizontalArrangement = Arrangement.Center,
        rows = TvGridCells.Fixed(3),
        contentPadding = PaddingValues(16.dp)
)

On the other hand, using the Adaptive approach will share the remaining space that falls outside of the provided minimum size.

TvLazyHorizontalGrid(
        verticalArrangement = Arrangement.Center,
        horizontalArrangement = Arrangement.Center,
        rows = TvGridCells.Adaptive(240.dp),
        contentPadding = PaddingValues(16.dp)
    )

TV Lazy vs Lazy

Now that we’ve discovered these initial TV Lazy Composables, you may be wondering, why do we have these different composables for TV apps?

The first difference is the scrolling behaviour of the Lazy layout composable. When navigating through the LazyVerticalGrid composable (be it on mobile or TV), we can see the follow behaviour:

We can see here how when we hit the end of the visible items in the list, we need to continue scrolling to see the next items. When navigating via touch input on a mobile device, this is fine because we can quickly and easily see the next items in the list when using our touch gesture. However, on TV, this is not an ideal experience as we have to click the navigational pad to see the ‘hidden’ items in the list. This results in the user carrying out more clicks in-order to browse further content in the collection. We can also see how the parent composable does not have sufficient padding to display the grid items, cutting off some of the items on the edge of the screen when they are currently in focus.

However, let’s take a look at the TvLazyVerticalGrid running on the same TV device:

We can see here how as we navigate through the content, the next items automatically become visible. This not only creates a greater immersive experience, but it previews the next content to the user – meaning that they do not need to manually navigate further to see what is next in the content stream.

Aside from this, TV composables have TV-specific APIs that hide properties that are not relevant on TV devices (e.g flingBehaviour), and expose properties that specific to TV experiences (e.g pivotOffsets). The composables will also allow us to ensure that TV-specific behaviours are automatically applied in our apps (such as focus management or navigation), as the standard lazy composables may not always have awareness of these concepts. With the exposed APIs being almost identical, there is not extra overhead in utilising these TV-specific composables.

As a general rule, use TV Lazy over the standard compose Lazy layouts. These allow us to enforce TV-specific behaviours and ensure that our components are always providing an optimal experience on TV devices.


Leave a Reply

Your email address will not be published. Required fields are marked *