Exploring Jetpack Compose for Android TV: Carousel

Joe Birch
Google Developer Experts
9 min readMar 8, 2023

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.

This post was originally shared on Droid Academy.

When we’re building apps for TVs, things work a little differently than when building apps for handheld devices. There are many different things that we need to take into account when building TV apps when compared to those for handheld devices, but I want to focus on a couple of points that are directly impacted by the UI framework.

  • The interaction with a TV is completely different than that of a phone — we don’t poke a TV with our fingers to navigate around an app, we instead use physical controllers to move around the screen to a chosen destination. Because of this, apps also need to clearly indicate where the user is currently at on the screen, ensuring that the currently focused element is indicated. Alongside needing composables that are specifically built for these larger screen experiences and navigation, we also need them to provide this level of indication based on their focused state.
  • TV is an immersive experience — while in handheld apps we often have text-heavy applications, TV is all about consuming visual content. We need composables that allow us to display this visual information to the user, in an array of formats.

While there is far more to think about when developing for Android TV, the above constraints are enough to warrant some TV friendly compose APIs. There are currently two dependencies available that offer the above functionalities, so before we get started with Compose for Android TV, we’ll need to add the following dependencies to your Compose project:

implementation 'androidx.tv:tv-foundation:1.0.0-alpha04'
implementation 'androidx.tv:tv-material:1.0.0-alpha04'

With the above in place, we can now move on to look at a composable that is built specifically for TV experiences, the Carousel. if you want to follow along using the sample code for this project, you can find this here.

Carousel

The Carousel is a UI component that is used to display key content to the user — this would be currently featured TV shows, films that are recommended to them based on previously viewed content, or anything else that should be shown prominently to the user. A Carousel is made up of several different slides, automatically moving between the different items after a specified amount of time.

For an example of this, let’s take a look at the Home Screen of an Android TV.

Here we can see several different elements that make up the Carousel:

  • Background Image — a full-screen immersive image for the current slide item
  • Content Area — used to display information about the highlighted item (such as a title and description)
  • Slide Indicator — shows the number of carousel items, along with which one is currently in view

It’s important to note here that the Carousel is designed for visual content — while we can provide content to be displayed on-top of the image, the visual element (background) should really sell the item that is currently in focus. The content area can be used to provide key information so that the user knows what the item is, but we don’t want to pollute this area with too much content.

When it comes to creating a Carousel of our own, the Compose TV APIs provide us with the following composable:

@Composable
fun Carousel(
slideCount: Int,
modifier: Modifier = Modifier,
carouselState: CarouselState = remember { CarouselState() },
autoScrollDurationMillis: Long = CarouselDefaults.TimeToDisplaySlideMillis,
contentTransformForward: ContentTransform = CarouselDefaults.contentTransform,
contentTransformBackward: ContentTransform = CarouselDefaults.contentTransform,
carouselIndicator:
@Composable BoxScope.() -> Unit = {
CarouselDefaults.IndicatorRow(
slideCount = slideCount,
activeSlideIndex = carouselState.activeSlideIndex,
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(16.dp),
)
},
content: @Composable CarouselScope.(index: Int) -> Unit
)

We can see a range of arguments support from the Carousel composable function, so let’s start composing a Carousel of our own so that we can understand what each of these arguments provides us with.

We’ll start with the required arguments of the Carousel, the slideCount and content. These arguments are directly related to the content of the Carousel, which are the individual items that are going to be displayed on-screen. When it comes to populating these properties, we’re going to have some of content list that makes up the items to be displayed inside of the Carousel. To keep things simple, I’ve created a simple data factory class that will be used to populate the Carousel. To start with, the slideCount consists of the number of items in this list, which we’ll access using the count() function.

When it comes to the content, this takes a composable function that will be used to compose the content of our Carousel.

val items = makeCarouselItems()
Carousel(
slideCount = items.count(),
content = { }
)

We can add an empty composable block here to satisfy the compiler, but we’re going to want to populate this with the children of the Carousel. For this there is a specific composable that we can use, the CarouselItem.

Carousel Item

When composing the Carousel, we need to provide a composable function to represent the content argument. For this, the Compose TV APIs provide us with a CarouselItem composable.

@Composable
fun CarouselItem(
modifier: Modifier = Modifier,
background: @Composable () -> Unit = {},
contentTransformForward: ContentTransform =
CarouselItemDefaults.contentTransformForward,
contentTransformBackward: ContentTransform =
CarouselItemDefaults.contentTransformBackward,
content: @Composable () -> Unit
)

When it comes to using this composable, we’re going to want to compose a child for each of the items which we want to display within our Carousel. The content function provides us with the index of the item that is being composed, so we’ll use this index to get the item, followed by composing a CarouselItem for that item from the list.

Carousel(
slideCount = items.count(),
content = { index ->
items[index].also { item ->
CarouselItem(...)
}
}
)

As this is though, the compiler is going to be showing us an error for this because we’re not providing the required arguments for the CarouselItem. Let’s take a look at what we need to provide in order to be able to compose the children of the Carousel.

Item Background

For each CarouselItem, we’re going to want to assign a background — this is what will be composed to fill the area of the item background, behind any content that is provided. While this isn’t required, it allows us to create an immersive component — remember that on a TV we really want to lean into the visual aspect, which we can do by utilising this background argument.

You can use any means to load an image here, but we’re going to keep things simple and use the AsyncImage composable from coil. Here we’ll pass it the image property from our TvItem class, followed by using the Crop ContentScale type so that our image is cropped to fit within the area of the CarouselItem. We won’t provide a content description here, as we are going to be providing content to be composed over the background, which will be providing the description for the current item that is in focus.

CarouselItem(
background = {
AsyncImage(
model = item.image, contentDescription = null, contentScale = ContentScale.Crop
)
},
...
}

When composing this inside of our parent Carousel, we’ll now see each of the child items being represented by the CarouselItem composable. Here we’ll be able to see the composition of each item, along with each item being navigated between.

Item Content

Now that we have the background of our CarouselItem being composed, we’re going to want to populate the content argument in the form of a composable. For this we’re going to display 3 pieces of information:

  • The title of the film
  • A description for a film
  • What service the film is available on

This part isn’t isolated to Compose for Android TV, so we won’t focus on the details too much here. Here we are simply creating a Box that displays the above pieces of information, stacked in a vertical arrangement.

@Composable
fun HomeItemBody(item: TvItem) {
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(
Color.Black.copy(alpha = 0.7f),
Color.Transparent
)
)
), contentAlignment = Alignment.TopStart
) {
Column(
modifier = Modifier
.width(500.dp)
.wrapContentHeight()
.padding(32.dp)
) {
Text(
text = item.title, fontSize = 32.sp, color = Color.White
)
Spacer(modifier = Modifier.height(12.dp))
Text(
text = item.description,
fontSize = 16.sp,
maxLines = 3,
overflow = TextOverflow.Ellipsis,
color = Color.White
)
Spacer(modifier = Modifier.height(12.dp))
Row {
Text(
text = stringResource(id = R.string.label_watch_on), fontSize = 14.sp, color = Color.White, fontWeight = FontWeight.Bold
)
Text(
text = item.watchOn, fontSize = 14.sp, color = Color.White
)
}
}
}
}

Viewing the preview of this composable gives us the following result:

With this in place, we can now plug this into the body of of our CarouselItem composable, passing the instance of the TvItem that is to be used for composition of the item body.

CarouselItem(
background = {
AsyncImage(
item.image, null, contentScale = ContentScale.Crop
)
},
content = {
HomeItemBody(item)
}
)

Now we’ll be able to see the entirety of our CarouselItem being composed for each TvItem.

Animating Item Visibility

Alongside the arguments that we explored above for the CarouselItem, you’ll also find the contentTransformForward and contentTransformBackward arguments — both of these are used to control how the CarouselItem composable animates in and out of view. Both of these arguments take a ContentTransform type which can be found in the Compose animation APIs. Because of this, f we want to override the default animations then we can simply use compose animations that we are already familiar with. For examples sake, I’m going to create a simple fade in/out animation using the ContentTransform class.

val transform = ContentTransform(
targetContentEnter = fadeIn(tween(durationMillis = 1000)),
initialContentExit = fadeOut(tween(durationMillis = 1000))
)

Finally, we’ll pass this reference to both of the contentTransformForward and contentTransformBackward arguments.

CarouselItem(
contentTransformForward = transform,
contentTransformBackward = transform,
...
)

Controlling the Carousel Slide duration

Before we wrap up, I want to take a look at one final argument for the Carousel composable — the autoScrollDurationMillis argument, which allow us to control the how long each child of the Carousel should be displayed for before moving to the next item. by default this is set to 5000 milliseconds, but you can override this by providing a Long value for this argument.

Carousel(
modifier = modifier,
carouselState = state,
autoScrollDurationMillis = 7500,
slideCount = items.count(),
content = { }
)

Wrapping up

In this blog post we’ve dipped our toes into Jetpack Compose for Android TV, learning about the Carousel composable and how it can be used to show immersive suggestions on screen. In future posts, we’ll explore how we can overlay content on top of the Carousel, allowing the user to navigate through content while still presenting this hero card rotator on-screen.

Stay tuned for the next post where we’ll dive deeper into Compose for Android TV 📺

--

--

Joe Birch
Google Developer Experts

Android @ Buffer, Google Developer Expert for Android. Passionate about mobile development and learning. www.joebirch.co