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 を修了してください。
  • コンポーザブルと修飾子に関する基本的な知識。

構築内容

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

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

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

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

必要なもの

2. セットアップする

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

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

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

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

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

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

カタログ ブラウザ画面では、映画のカタログをブラウジングできます。カタログ ブラウザを Composable 関数として実装します。CatalogBrowser Composable 関数は CatalogBrowser.kt ファイルにあります。この Composable 関数で、カタログ ブラウザ画面を実装します。

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

CatalogBrowser.kt

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import com.example.tvcomposeintroduction.data.Movie

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

カテゴリ名を表示する

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

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

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

CatalogBrowser.kt

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie

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

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

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

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

  1. TvLazyRow Composable 関数を追加して、ラムダを渡します。
  2. ラムダで、category を使用して items 関数を呼び出します。movieList 属性値を取得し、ラムダを渡します。
  3. items 関数に渡されたラムダで、Movie オブジェクトを指定して MovieCard Composable 関数を呼び出します。

CatalogBrowser.kt

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

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

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

  1. カテゴリ間にギャップを設定するには、verticalArrangement パラメータで Arrangement オブジェクトを TvLazyColumn Composable 関数に渡します。Arrangement オブジェクトは、Arrangement#spacedBy メソッドを呼び出して作成されます。
  2. 映画カードの間隔を設定するには、horizontalArrangement パラメータを使用して Arrangement オブジェクトを TvLazyRow Composable 関数に渡します。
  3. 列にインデントを設定するには、contentPadding パラメータで PaddingValue オブジェクトを渡します。

CatalogBrowser.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

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

4. 詳細画面を実装する

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

Details.kt

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.tv.material3.ExperimentalTvMaterial3Api
import com.example.tvcomposeintroduction.data.Movie

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

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

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

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

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

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

Details.kt

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie

@OptIn(ExperimentalTvMaterial3Api::class)
@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.title)
    }
}

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

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

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

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

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

Details.kt

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import com.example.tvcomposeintroduction.data.Movie

@OptIn(ExperimentalTvMaterial3Api::class)
@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
                .padding(vertical = 32.dp, horizontal = 48.dp)
        ) {
            Text(
                text = movie.title,
            )
            Text(
                text = movie.studio,
            )
            Text(
                text = movie.title,
            )
        }
    }
}

一貫性のあるテーマ設定については、MaterialTheme オブジェクトを参照してください。

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

テーマ設定の一貫性を保つために MaterialTheme オブジェクトを参照するには、次の手順を行います。

  1. MaterialTheme.typography.headlineLarge プロパティに、映画のタイトルのテキスト スタイルを設定します。
  2. MaterialTheme.typography.headlineMedium プロパティに、他の 2 つの Text Composable 関数のテキスト スタイルを設定します。
  3. Modifier.background メソッドで、MaterialTheme.colorScheme.background プロパティを Column Composable 関数の背景色に設定します。

[ColorScheme]: /reference/kotlin/androidx/tv/material3/ColorScheme)

Details.kt

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import com.example.tvcomposeintroduction.data.Movie

@OptIn(ExperimentalTvMaterial3Api::class)
@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
                .padding(vertical = 32.dp, horizontal = 48.dp)
        ) {
            Text(
                text = movie.title,
                style = MaterialTheme.typography.headlineLarge,
            )
            Text(
                text = movie.studio,
                style = MaterialTheme.typography.headlineMedium,
            )
            Text(
                text = movie.title,
                style = MaterialTheme.typography.headlineMedium,
            )
        }
    }
}

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

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

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

CatalogBrowser.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = viewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsState()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        items(categoryList) { category ->
            Text(text = category.name)
            TvLazyRow(
                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 関数を呼び出して、アイテムを TvLazyColumn Composable 関数に追加します。
  3. item 関数に渡されたラムダ内の委任プロパティとして featuredMovieList を宣言し、catalogBrowserViewModel.featuredMovieList 属性から収集される State オブジェクトを委任に設定します。
  4. item 関数内で Carousel Composable 関数を呼び出し、次のパラメータを渡します。
  • slideCount パラメータを介した featuredMovieList 変数のサイズ。
  • Modifier.fillMaxWidth メソッドと Modifier.height メソッドでカルーセル サイズを指定するための Modifier オブジェクト。Carousel Composable 関数は、376.dp の値を Modifier.height メソッドに渡すことで、高さ 376 dp を使用します。
  • 表示されるカルーセル アイテムのインデックスを示す整数値で呼び出されるラムダ。
  1. featuredMovieList 変数と指定されたインデックス値から Movie オブジェクトを取得します。
  2. CarouselSlide Composable 関数を Carousel Composable 関数に追加します。
  3. 映画のタイトルを表示するには、CarouselSlide Composable 関数に Text Composable 関数を追加します。

CatalogBrowser.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Carousel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

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

背景画像を表示する

CarouselSlide Composable 関数は、別のラムダを使用して、CarouselSlide Composable 関数の背景をどのように表示するかを指定できます。

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

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

CatalogBrowser.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Carousel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import com.example.tvcomposeintroduction.R
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

@OptIn(ExperimentalTvMaterial3Api::class)
@Composable
fun CatalogBrowser(
    modifier: Modifier = Modifier,
    catalogBrowserViewModel: CatalogBrowserViewModel = viewModel(),
    onMovieSelected: (Movie) -> Unit = {}
) {
    val categoryList by
    catalogBrowserViewModel.categoryList.collectAsState()
    TvLazyColumn(
        modifier = modifier,
        verticalArrangement = Arrangement.spacedBy(16.dp),
        contentPadding = PaddingValues(horizontal = 48.dp, vertical = 32.dp)
    ) {
        item {
            val featuredMovieList by catalogBrowserViewModel.featuredMovieList.collectAsState()
            Carousel(
                slideCount = featuredMovieList.size,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(376.dp),
            ) { indexOfCarouselItem ->
                val featuredMovie = featuredMovieList[indexOfCarouselItem]
                CarouselSlide(
                    background = {
                        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)
            TvLazyRow(
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(category.movieList) { movie ->
                    MovieCard(movie = movie, onClick = { onMovieSelected(movie) })
                }
            }
        }
    }
}

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

ユーザーに CarouselSlide Composable 関数のクリックを許可できます。

詳細画面のカルーセル アイテムでユーザーが映画の詳細を確認できるようにするには、次の手順を行います。

  1. modifier パラメータを通じて、Modifier.clickable メソッドの戻り値を CarouselSlide Composable 関数に渡します。
  2. Modifier.clickable メソッドに渡されるラムダ内の可視 CarouselSlideComposable 関数に対して、Movie オブジェクトを指定して onMovieSelected 関数を呼び出します。

CatalogBrowser.kt

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.tv.foundation.lazy.list.TvLazyColumn
import androidx.tv.foundation.lazy.list.TvLazyRow
import androidx.tv.foundation.lazy.list.items
import androidx.tv.material3.Carousel
import androidx.tv.material3.ExperimentalTvMaterial3Api
import androidx.tv.material3.Text
import coil.compose.AsyncImage
import com.example.tvcomposeintroduction.R
import com.example.tvcomposeintroduction.data.Movie
import com.example.tvcomposeintroduction.ui.components.MovieCard

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

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 の基本を習得しました。

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