Compose for TV の概要

1. 始める前に

Compose for TV は、Android TV で動作するアプリを開発するための最新の UI フレームワークです。TV アプリ向け Jetpack Compose のすべてのメリットを活用できるため、アプリの美しく機能性の高い UI の作成が容易になります。Compose for TV の具体的なメリットは次のとおりです。

  • 柔軟性 Compose を使用すると、シンプルなレイアウトから複雑なアニメーションまで、あらゆる種類の UI を作成できます。コンポーネントはすぐに使用できますが、アプリのニーズに合わせたカスタマイズやスタイル設定も可能です。
  • 開発の簡素化と加速Compose は既存のコードと互換性があるため、デベロッパーは少ないコードでアプリを構築できます。
  • 直感的: Compose では、UI の変更とコードのデバッグ、理解、レビューを直感的に行える宣言型構文が使用されています。

TV アプリの一般的なユースケースはメディア消費です。ユーザーはコンテンツ カタログを参照して、視聴するコンテンツを選択します。コンテンツには、ムービー、テレビ番組、ポッドキャストなどがあります。ユーザーがコンテンツの選択を終えると、簡単な説明、再生時間、クリエイターの名前などの詳細情報が表示される可能性があります。この Codelab では、Compose for TV を使用してカタログ ブラウザ画面と詳細画面を実装する方法を学びます。

前提条件

  • ラムダを含む Kotlin 構文の使用経験。
  • Compose に関する基本的な経験。Compose に慣れていない場合は、Jetpack Compose の基本の Codelab を修了してください。
  • コンポーザブルと修飾子に関する基本的な知識。
  • 次のいずれかのデバイス(サンプルアプリの実行用):
    • Android TV デバイス
    • プロファイルが TV デバイス定義カテゴリに属する Android 仮想デバイス

構築内容

  • カタログ ブラウザ画面と詳細画面を備えた動画プレーヤー アプリ。
  • ユーザーが選択できる動画の一覧が表示されるカタログ ブラウザ画面。次の画像のように表示されます。

カタログ ブラウザは、上部のカルーセルで注目の映画の\nリストを表示します。\n画面にはカテゴリごとの映画のリストも表示されます。

  • タイトル、説明、長さなど、選択した動画のメタデータを表示する詳細画面。次の画像のように表示されます。

詳細画面には、映画のメタデータ(タイトル、スタジオ、簡単な説明など)\nが表示されます。\nメタデータは、映画に関連付けられている背景画像に表示されます。

必要なもの

  • Android Studio の最新バージョン
  • TV デバイス カテゴリに属する Android TV デバイスまたは仮想デバイス

2. セットアップする

この Codelab のテーマ設定と基本設定を含むコードを取得するには、次のいずれかを行います。

$ git clone https://github.com/android/tv-codelabs.git

main ブランチにはスターター コードが含まれ、solution ブランチには解答コードが含まれます。

  • スターター コードを含む main.zip ファイルと、解答コードを含む solution.zip ファイルをダウンロードします。

コードがダウンロードされたところで、Android Studio で IntroductionToComposeForTV プロジェクト フォルダを開きます。これで準備が整いました。

3.カタログ ブラウザ画面を実装する

カタログ ブラウザ画面では、映画のカタログをブラウジングできます。カタログ ブラウザをコンポーズ可能な関数として実装します。コンポーズ可能な関数 CatalogBrowserCatalogBrowser.kt ファイルにあります。このコンポーズ可能な関数で、カタログ ブラウザ画面を実装します。

スターター コードには、CatalogBrowserViewModel クラスと呼ばれる ViewModel があります。このクラスには、映画のコンテンツを記述する Movie オブジェクトを取得するための属性とメソッドがいくつかあります。取得した Movie オブジェクトを使用してカタログ ブラウザを実装します。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
}

カテゴリ名を表示する

Category リストのフローである catalogBrowserViewModel.categoryList 属性を使用して、カテゴリのリストにアクセスできます。フローは、collectAsStateWithLifecycle メソッドを呼び出すことにより Compose State オブジェクトとして収集されます。Category オブジェクトには name 属性があります。この属性は、カテゴリ名を表す String 値です。

カテゴリ名を表示する手順は次のとおりです。

  1. Android Studio でスターター コードの CatalogBrowser.kt ファイルを開き、コンポーズ可能な関数 LazyColumn をコンポーズ可能な関数 CatalogBrowser に追加します。
  2. catalogBrowserViewModel.categoryList.collectAsStateWithLifeCycle() メソッドを呼び出して、フローを State オブジェクトとして収集します。
  3. categoryList は、前の手順で作成した State オブジェクトの委任プロパティとして宣言します。
  4. categoryList 変数をパラメータとして items 関数を呼び出します。
  5. ラムダの引数として渡されるパラメータとしてカテゴリ名を指定し、コンポーズ可能な関数 Text を呼び出します。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    LazyColumn(modifier = modifier) {
        items(categoryList) { category ->
            Text(text = category.name)
        }
    }
}

各カテゴリのコンテンツ リストを表示する

Category オブジェクトには、movieList という属性があります。この属性は、カテゴリに属する映画を表す Movie オブジェクトのリストです。

各カテゴリのコンテンツ リストを表示する手順は次のとおりです。

  1. コンポーズ可能な関数 LazyRow を追加してラムダを渡します。
  2. ラムダ内で、category を使用して items 関数を呼び出します。movieList 属性値を取得してラムダを渡します。
  3. items 関数に渡されたラムダ内で、Movie オブジェクトを指定してコンポーズ可能な関数 MovieCard を呼び出します。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    LazyColumn(modifier = modifier) {
        items(categoryList) { category ->
            Text(text = category.name)
            LazyRow {
                items(category.movieList) {movie ->
                    MovieCard(movie = movie)
                }
            }
        }
    }
}

省略可: レイアウトを調整する

  1. カテゴリ間の間隔を設定するには、verticalArrangement パラメータを使用して Arrangement オブジェクトをコンポーズ可能な関数 LazyColumn に渡します。Arrangement オブジェクトは、Arrangement#spacedBy メソッドを呼び出すことにより作成されます。
  2. 映画カードの間隔を設定するには、horizontalArrangement パラメータを使用して Arrangement オブジェクトをコンポーズ可能な関数 LazyRow に渡します。
  3. 列にインデントを設定するには、contentPadding パラメータで PaddingValue オブジェクトを渡します。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifeCycle()
    LazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        items(categoryList) { category ->
            Text(text = category.name)
            LazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie)
                }
            }
        }
    }
}

4. 詳細画面を実装する

詳細画面には、選択した映画の詳細が表示されます。Details.kt ファイル内に、コンポーズ可能な関数 Details があります。この関数にコードを追加して、詳細画面を実装します。

Details.kt

@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
}

ムービーのタイトル、スタジオ名、説明を表示する

Movie オブジェクトには、映画のメタデータとして次の 3 つの文字列属性があります。

  • title。映画のタイトル。
  • studio。映画を制作したスタジオの名前。
  • description。映画の簡単な要約。

このメタデータを詳細画面に表示する手順は次のとおりです。

  1. コンポーズ可能な関数 Column を追加し、Modifier.padding メソッドで作成した Modifier オブジェクトを使用して、縦方向 32 dp と横方向 48 dp のクリアランスを列に設定します。
  2. 映画のタイトルを表示するには、コンポーズ可能な関数 Text を追加します。
  3. スタジオ名を表示するには、コンポーズ可能な関数 Text を追加します。
  4. 映画の説明を表示するには、コンポーズ可能な関数 Text を追加します。

Details.kt

@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
    Column(
        modifier = Modifier
            .padding(vertical = 32.dp, horizontal = 48.dp)
    ) {
        Text(text = movie.title)
        Text(text = movie.studio)
        Text(text = movie.description)
    }
}

コンポーズ可能な関数 Details のパラメータで指定された Modifier オブジェクトは、次のタスクで使用します。

特定の Movie オブジェクトに関連付けられている背景画像を表示する

Movie オブジェクトには backgroundImageUrl 属性があり、そのオブジェクトで記述されている映画の背景画像の場所が示されます。

特定の映画の背景画像を表示する手順は次のとおりです。

  1. コンポーズ可能な関数 Box をコンポーズ可能な関数 Column のラッパーとして追加し、modifier オブジェクトをコンポーズ可能な関数 Details を通じて渡します。
  2. コンポーズ可能な関数 Box 内で、modifier オブジェクトの fillMaxSize メソッドを呼び出して、コンポーズ可能な関数 Box がコンポーズ可能な関数 Details に割り当てることのできる最大サイズを満たすようにします。
  3. 次のパラメータを含むコンポーズ可能な関数 AsyncImage をコンポーズ可能な関数 Box に追加します。
  • 指定された Movie オブジェクトの backgroundImageUrl 属性の値を model パラメータに設定します。
  • nullcontentDescription パラメータに渡します。
  • ContentScale.Crop オブジェクトを contentScale パラメータに渡します。さまざまな ContentScale オプションについては、コンテンツ スケールをご覧ください。
  • Modifier.fillMaxSize メソッドの戻り値を modifier パラメータに渡します。

Details.kt

@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize()) {
        AsyncImage(
            model = movie.cardImageUrl,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
        Column {
            Text(
                text = movie.title,
            )
            Text(
                text = movie.studio,
            )
            Text(text = movie.description)
        }
    }
}

テーマ設定の一貫性を保つために MaterialTheme オブジェクトを参照する

MaterialTheme オブジェクトには、Typography クラスや ColorScheme クラスなどにある現在のテーマ値を参照する関数が含まれています。

テーマ設定の一貫性を保つために MaterialTheme オブジェクトを参照する手順は次のとおりです。

  1. MaterialTheme.typography.displayMedium プロパティに、映画のタイトルのテキスト スタイルを設定します。
  2. MaterialTheme.typography.bodySmall プロパティに、2 番目のコンポーズ可能な関数 Text のテキスト スタイルを設定します。
  3. Modifier.background メソッドを使用して、MaterialTheme.colorScheme.background プロパティに、コンポーズ可能な関数 Column の背景色を設定します。

Details.kt

@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize()) {
        AsyncImage(
            model = movie.cardImageUrl,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
        Column(
            modifier = Modifier
                .background(MaterialTheme.colorScheme.background),
        ) {
            Text(
                text = movie.title,
                style = MaterialTheme.typography.displayMedium,
            )
            Text(
                text = movie.studio,
                style = MaterialTheme.typography.bodySmall,
            )
            Text(text = movie.description)
        }
    }
}

省略可: レイアウトを調整する

コンポーズ可能な関数 Details のレイアウトを調整する手順は次のとおりです。

  1. fillMaxSize 修飾子により、使用可能なスペース全体を使用するようにコンポーズ可能な関数 Box を設定します。
  2. background 修飾子でコンポーズ可能な関数 Box の背景を設定し、MaterialTheme.colorScheme.background 値と Color.Transparent を含む Color オブジェクトのリストで Brush.linearGradient 関数を呼び出すことにより作成される線形グラデーションで背景を塗りつぶします。
  3. padding 修飾子を使用して、コンポーズ可能な関数 Column の周囲に横方向 48.dp と縦方向 24.dp のクリアランスを設定します。
  4. 0.5f 値を指定して Modifier.width 関数を呼び出すことにより作成される width 修飾子を使用して、コンポーズ可能な関数 Column の幅を設定します。
  5. Spacer を使用して、2 番目のコンポーズ可能な関数 Text と 3 番目のコンポーズ可能な関数 Text の間に 8.dp のスペースを追加します。コンポーズ可能な関数 Spacer の高さは、Modifier.height 関数で作成された height 修飾子で指定します。

Details.kt

@Composable
fun Details(movie: Movie, modifier: Modifier = Modifier) {
    Box(modifier = modifier.fillMaxSize()) {
        AsyncImage(
            model = movie.cardImageUrl,
            contentDescription = null,
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
        Box(
            modifier = Modifier
                .background(
                    Brush.linearGradient(
                        listOf(
                            MaterialTheme.colorScheme.background,
                            Color.Transparent
                        )
                    )
                )
                .fillMaxSize()
        ) {
            Column(
                modifier = Modifier
                    .padding(horizontal = 48.dp, vertical = 24.dp)
                    .fillMaxWidth(0.5f)
            ) {
                Text(
                    text = movie.title,
                    style = MaterialTheme.typography.displayMedium,
                )
                Text(
                    text = movie.studio,
                    style = MaterialTheme.typography.bodySmall,
                )
                Spacer(modifier = Modifier.height(8.dp))
                Text(
                    text = movie.description,
                )
            }
        }
    }
}

5. 画面間のナビゲーションを追加する

これで、カタログ ブラウザと詳細画面が表示されます。ユーザーがカタログ ブラウザ画面でコンテンツを選択した後は、詳細画面に遷移する必要があります。そのためには、clickable 修飾子を使用して、event リスナーをコンポーズ可能な関数 MovieCard に追加します。十字キーの中央ボタンが押されると、コンポーズ可能な関数 MovieCard に関連付けられた映画オブジェクトを引数として CatalogBrowserViewModel#showDetails メソッドが呼び出されます。

  1. com.example.tvcomposeintroduction.ui.screens.CatalogBrowser ファイルを開きます。
  2. onClick パラメータを指定して、ラムダ関数をコンポーズ可能な関数 MovieCard に渡します。
  3. コンポーズ可能な関数 MovieCard に関連付けられた映画オブジェクトを指定して、onMovieSelected コールバックを呼び出します。

CatalogBrowser.kt

@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    LazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        items(categoryList) { category ->
            Text(text = category.name)
            LazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

6. カタログ ブラウザの画面にカルーセルを追加して、注目のコンテンツをハイライト表示する

カルーセルは、指定された時間が経過するとスライドを自動的に更新する、適応性の高い UI コンポーネントです。通常は、注目のコンテンツをハイライト表示するために使用します。

カルーセルをカタログ ブラウザ画面に追加して、注目のコンテンツ リストで映画をハイライトする手順は次のとおりです。

  1. com.example.tvcomposeintroduction.ui.screens.CatalogBrowser ファイルを開きます。
  2. item 関数を呼び出して、アイテムをコンポーズ可能な関数 LazyColumn に追加します。
  3. item 関数に渡されたラムダ内の委任プロパティとして featuredMovieList を宣言し、catalogBrowserViewModel.featuredMovieList 属性から収集される State オブジェクトを委任に設定します。
  4. item 関数内でコンポーズ可能な関数 Carousel を呼び出し、次のパラメータを渡します。
  • slideCount パラメータを介した featuredMovieList 変数のサイズ。
  • Modifier.fillMaxWidth メソッドと Modifier.height メソッドでカルーセル サイズを指定するための Modifier オブジェクト。コンポーズ可能な関数 Carousel は、376.dp 値を Modifier.height メソッドに渡して、376 dp の高さを使用します。
  • 表示されるカルーセル アイテムのインデックスを示す整数値で呼び出されるラムダ。
  1. featuredMovieList 変数と指定されたインデックス値から Movie オブジェクトを取得します。
  2. コンポーズ可能な関数 Box をコンポーズ可能な関数 Carousel に追加します。
  3. 映画のタイトルを表示するには、コンポーズ可能な関数 Text をコンポーズ可能な関数 Box に追加します。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    LazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsStateWithLifecycle()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp)
            ) { indexOfCarouselSlide ->
                val featuredMovie =
                    featuredMovieList[indexOfCarouselSlide]
                Box {
                    Text(text = featuredMovie.title)
                }
            }
        }
        items(categoryList) { category ->
            Text(text = category.name)
            LazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

背景画像を表示する

コンポーズ可能な関数 Box は、あるコンポーネントを別のコンポーネントの上に配置します。詳しくは、レイアウトの基本をご覧ください。

背景画像を表示する手順は次のとおりです。

  1. コンポーズ可能な関数 AsyncImage を呼び出して、コンポーズ可能な関数 Text の前で、Movie オブジェクトに関連付けられた背景画像を読み込みます。
  2. 視認性を高めるため、コンポーズ可能な関数 Text の位置とテキスト スタイルを更新します。
  3. レイアウト シフトを避けるため、プレースホルダにコンポーズ可能な関数 AsyncImage を設定します。スターター コードには、R.drawable.placeholder で参照できるドローアブルとしてプレースホルダがあります。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    LazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsStateWithLifecycle()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                Box{
                    AsyncImage(
                        model = featuredMovie.backgroundImageUrl,
                        contentDescription = null,
                        placeholder = painterResource(
                            id = R.drawable.placeholder
                        ),
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.fillMaxSize(),
                    )
                    Text(text = featuredMovie.title)
                }
            }
        }
        items(categoryList) { category ->
            Text(text = category.name)
            LazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

詳細画面への画面遷移を追加する

Button をカルーセルに追加すると、ユーザーがボタンをクリックして詳細画面への画面遷移をトリガーできるようになります。

詳細画面に表示されるカルーセルでユーザーが映画の詳細を確認できるようにする手順は次のとおりです。

  1. Carousel コンポーザブル内の Box コンポーザブルで、コンポーズ可能な関数 Column を呼び出します。
  2. CarouselText コンポーザブルをコンポーズ可能な関数 Column に移動します。
  3. コンポーズ可能な関数 Column 内のコンポーズ可能な関数 Text の後で、コンポーズ可能な関数 Button を呼び出します。
  4. R.string.show_details で呼び出された stringResource 関数の戻り値を使用して、コンポーズ可能な関数 Button 内でコンポーズ可能な関数 Text を呼び出します。
  5. コンポーズ可能な関数 ButtononClick パラメータに渡されたラムダ内で featuredMovie 変数を指定して、onMovieSelected 関数を呼び出します。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    LazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsStateWithLifecycle()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                Box {
                    AsyncImage(
                        model = featuredMovie.backgroundImageUrl,
                        contentDescription = null,
                        placeholder = painterResource(
                            id = R.drawable.placeholder
                        ),
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.fillMaxSize(),
                    )
                    Column {
                        Text(text = featuredMovie.title)
                        Button(onClick = { onMovieSelected(featuredMovie) }) {
                            Text(text = stringResource(id = R.string.show_details))
                        }
                    }
                }
            }
        }
        items(categoryList) { category ->
            Text(text = category.name)
            LazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

省略可: レイアウトを調整する

カルーセルのレイアウトを調整する手順は次のとおりです。

  1. コンポーズ可能な関数 Carousel 内で、backgroundColor 値に MaterialTheme.colorScheme.background 値を代入します。
  2. コンポーズ可能な関数 ColumnBox コンポーザブルでラップします。
  3. Alignment.BottomStart 値を Box コンポーネントの contentAlignment パラメータに渡します。
  4. fillMaxSize 修飾子をコンポーズ可能な関数 Box の修飾子パラメータに渡します。fillMaxSize 修飾子は Modifier.fillMaxSize() 関数で作成されます。
  5. Box コンポーザブルに渡された fillMaxSize 修飾子に対して drawBehind() メソッドを呼び出します。
  6. drawBehind 修飾子に渡されたラムダ内で、Brush オブジェクトを使用して brush 値を代入します。このオブジェクトは、2 つの Color オブジェクトのリストを指定して Brush.linearGradient 関数を呼び出すことにより作成されます。このリストは、backgroundColor 値と Color.Transparent 値を指定して listOf 関数を呼び出すことにより作成されます。
  7. drawBehind 修飾子に渡されたラムダ内の brush オブジェクトを指定して drawRect を呼び出し、背景画像の上にスクリムレイヤを作成します。
  8. padding 修飾子を使用して、コンポーズ可能な関数 Column のパディングを指定します。この修飾子は、20.dp 値を指定して Modifier.padding を呼び出すことにより作成されます。
  9. コンポーズ可能な関数 Column 内で、Text コンポーザブルと Button コンポーザブルの間に、20.dp の値を含むコンポーズ可能な関数 Spacer を追加します。

CatalogBrowser.kt

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = hiltViewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by catalogBrowserViewModel.categoryList.collectAsStateWithLifecycle()
    LazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(32.dp),
        contentPadding = PaddingValues(horizontal = 58.dp, vertical = 36.dp)
    ) {
        item {
            val featuredMovieList by
            catalogBrowserViewModel.featuredMovieList.collectAsStateWithLifecycle()

            Carousel(
                itemCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                val backgroundColor = MaterialTheme.colorScheme.background
                
                Box {
                    AsyncImage(
                        model = featuredMovie.backgroundImageUrl,
                        contentDescription = null,
                        placeholder = painterResource(
                            id = R.drawable.placeholder
                        ),
                        contentScale = ContentScale.Crop,
                        modifier = Modifier.fillMaxSize(),
                    )
                    Box(
                        contentAlignment = Alignment.BottomStart,
                        modifier = Modifier
                            .fillMaxSize()
                            .drawBehind {
                                val brush = Brush.horizontalGradient(
                                    listOf(backgroundColor, Color.Transparent)
                                )
                                drawRect(brush)
                            }
                    ) {
                        Column(
                            modifier = Modifier.padding(20.dp)
                        ) {
                            Text(
                                text = featuredMovie.title,
                                style = MaterialTheme.typography.displaySmall
                            )
                            Spacer(modifier = Modifier.height(28.dp))
                            Button(onClick = { onMovieSelected(featuredMovie) }) {
                                Text(text = stringResource(id = R.string.show_details))
                            }
                        }
                    }
                }
            }
        }
        items(categoryList) { category ->
            Text(text = category.name)
            LazyRow(
                horizontalArrangement = Arrangement.spacedBy(16.dp),
                modifier = Modifier.height(200.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(
                        movie,
                        onClick = {
                            onMovieSelected(it)
                        }
                    )
                }
            }
        }
    }
}

7. 解答コードを取得する

この Codelab の解答コードをダウンロードするには、次のいずれかを行います。

  • 次のボタンをクリックして ZIP ファイルとしてダウンロードし、Android Studio で開きます。

  • Git を使用して取得します。
$ git clone https://github.com/android/tv-codelabs.git
$ cd tv-codelabs
$ git checkout solution
$ cd IntroductionToComposeForTV

8. これで完了です。

お疲れさまでした。Compose for TV の基本を習得しました。

  • LazyColumn と LazyLow を組み合わせてコンテンツ リストを表示する画面の実装方法。
  • コンテンツの詳細を表示する基本的な画面の実装。
  • 2 つの画面間の画面遷移を追加する方法。