Lazy Columns And Rows for Android TV with 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 scrollable content in apps, we currently have a collection of Lazy APIs 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?


TvLazyRow

The TVLazyRow is the TV equivalent of the LazyRow from the Jetpack Compose APIs. This allows us to display horizontally scrolling content, composing items as they are required, lazily.

When it comes to displaying browsable content in your TV app, the TvLazyRow is going to be your go-to composable. Specifically when composable subsets of data, the TvLazyRow allows you to display a single row of content. If you’re looking to display larger sets of information, we’ll be looking at grid composables in the next blog post.

The TVLazyRow composable has an almost identical API to that of the LazyRow composable.

@Composable
fun TvLazyRow(
    modifier: Modifier = Modifier,
    state: TvLazyListState = rememberTvLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    horizontalArrangement: Arrangement.Horizontal =
        if (!reverseLayout) Arrangement.Start else Arrangement.End,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    userScrollEnabled: Boolean = true,
    pivotOffsets: PivotOffsets = PivotOffsets(),
    content: TvLazyListScope.() -> Unit
)

There are a couple of notable differences when it comes to the TvLazyRow and LazyRow composables.

  • The TvLazyRow contains no flingBehaviour argument
    • For the LazyRow, this is used to respond to user scroll gestures, which is not an available behaviour for TV devices

  • TvLazyRow contains an additional PivotOffsets argument
    • Due to the hardware input used to navigate on Android TV, the TvLazyRow 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 TvLazyRow, so is not available for the LazyRow 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 TvLazyRow, we can use the following code:

TvLazyRow(
    contentPadding = PaddingValues(16.dp),
    horizontalArrangement = Arrangement.spacedBy(18.dp)
) {
    items(data) { cardItem ->
        ...
    }
}

TvLazyColumn

Similar to the TvLazyRow, the TvLazyColumn is an equal replacement to the LazyColumn from the Compose APIs. This allows us to display vertically scrolling content, composing items in a Lazy fashion as they are required.

In some cases, you will be able to use this to display browsable content to the user. In most situations for TV apps, you’ll likely be using the TvLazyColumn to group other content composables or to compose settings/options screens.

The TVLazyColumn composable has an almost identical API to that of the LazyColumn composable.

@Composable
fun TvLazyColumn(
    modifier: Modifier = Modifier,
    state: TvLazyListState = rememberTvLazyListState(),
    contentPadding: PaddingValues = PaddingValues(0.dp),
    reverseLayout: Boolean = false,
    verticalArrangement: Arrangement.Vertical =
        if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    userScrollEnabled: Boolean = true,
    pivotOffsets: PivotOffsets = PivotOffsets(),
    content: TvLazyListScope.() -> Unit
)

We can again compose this in a similar way to the existing LazyColumn.

TvLazyColumn(
    contentPadding = PaddingValues(16.dp),
    verticalArrangement = Arrangement.spacedBy(18.dp)
) {
    items(data) { cardItem ->;
        // compose child items
    }
}

Combining the TvLazyRow and TvLazyColumn

In cases where we want to show collections of categorised content, we can combine the TvLazyRow and TvLazyColumn. In the example below, we can see different kinds of grouped content that can be scrolled horizontally, allowing the user to move vertically between each of the collections.

In this scenario, we can nest TvLazyRow composables inside of the TvLazyColumn composable, just like we would when it comes to the LazyRow and LazyColumn. This allows us to still provide a Tv-specific experience to our users via the TV compose APIs. You may also notice above how the focus position is persisted when moving between the nested rows in the parent column.

TvLazyColumn {
        item {
            Text(...)
        }
        item {
            TvLazyRow {
                items(data) { cardItem ->
                    ...
                }
            }
        }

        item {
            Text(...)
        }

        item {
            TvLazyRow {
                items(data) { cardItem ->
                    ...
                }
            }
        }

        item {
            Text(...)
        }

        item {
            TvLazyRow {
                items(data) { cardItem ->
                    ...
                }
            }
        }

        item {
            Text(...)
        }

        item {
            TvLazyRow {
                items(data) { cardItem ->
                    ...
                }
            }
        }
}

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 LazyRow 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.

However, let’s take a look at the TvLazyRow 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.


In this blog post, we’ve learned about the TV-specific lazy composables, TvLazyRow and TvLazyColumn. Stay tuned for the next post where we’ll take a look at the Lazy Grid APIs for Android TV using Jetpack Compose!

Leave a Reply

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