TV용 Compose 소개

1. 시작하기 전에

TV용 Compose는 Android TV에서 실행되는 앱을 개발하는 최신 UI 프레임워크입니다. 이를 통해 TV 앱용 Jetpack Compose의 모든 이점을 활용할 수 있으므로 디자인과 기능이 뛰어난 앱 UI를 더 쉽게 빌드할 수 있습니다. TV용 Compose의 구체적인 이점은 다음과 같습니다.

  • 유연성. Compose를 사용하면 간단한 레이아웃부터 복잡한 애니메이션에 이르기까지 모든 유형의 UI를 만들 수 있습니다. 구성요소는 즉시 사용할 수 있지만 앱의 요구사항에 맞게 맞춤설정하거나 스타일을 지정할 수 있습니다.
  • 개발 간소화 및 가속화. Compose는 기존 코드와 호환되며 개발자가 더 적은 코드로 앱을 빌드할 수 있습니다.
  • 직관적. Compose는 선언적 문법을 사용하므로, 직관적으로 UI를 변경하고 코드를 이해하고 디버그 및 검토할 수 있습니다.

TV 앱의 일반적인 사용 사례는 미디어 소비입니다. 사용자는 콘텐츠 카탈로그를 둘러보고 시청할 콘텐츠를 선택합니다. 콘텐츠는 영화, TV 프로그램 또는 팟캐스트일 수 있습니다. 사용자는 콘텐츠를 선택한 후에 간략한 설명, 재생 시간, 크리에이터 이름 등 자세한 내용을 확인하고 싶을 수 있습니다. 이 Codelab에서는 TV용 Compose를 사용하여 카탈로그 브라우저 화면과 세부정보 화면을 구현하는 방법을 알아봅니다.

기본 요건

  • 람다를 포함한 Kotlin 문법 사용 경험
  • 기본적인 Compose 사용 경험. Compose에 익숙하지 않다면 Jetpack Compose 기본사항 Codelab을 완료하세요.
  • 컴포저블과 수정자에 관한 기본 지식
  • 샘플 앱을 실행할 다음 기기 중 하나:
    • Android TV 기기
    • TV 기기 정의 카테고리에 프로필이 있는 Android 가상 기기

빌드할 항목

  • 카탈로그 브라우저 화면과 세부정보 화면이 있는 동영상 플레이어 앱
  • 사용자가 선택하도록 동영상 목록을 보여주는 카탈로그 브라우저 화면. 아래 이미지와 같이 표시됩니다.

카탈로그 브라우저에는 위에 캐러셀이 있는 추천 동영상\n목록이 표시됩니다.\n화면에 각 카테고리의 영화 목록도 표시됩니다.

  • 선택한 동영상에 관한 제목, 설명, 길이 등의 메타데이터를 보여주는 세부정보 화면. 아래 이미지와 같이 표시됩니다.

세부정보 화면에는 영화에 관해 제목, 스튜디오, 간략한 설명\n등의 메타데이터가 표시됩니다.\n메타데이터는 영화와 연결된 배경 이미지에 표시됩니다.

필요한 항목

  • 최신 버전의 Android 스튜디오
  • Android TV 기기 또는 TV 기기 카테고리의 가상 기기

2. 설정

이 Codelab의 테마 설정과 기본 설정이 포함된 코드를 가져오려면 다음 중 하나를 실행합니다.

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

main 브랜치에는 시작 코드가 있고 solution 브랜치에는 솔루션 코드가 포함되어 있습니다.

  • 시작 코드가 포함된 main.zip 파일과 솔루션 코드가 포함된 solution.zip 파일을 다운로드합니다.

코드를 다운로드했으므로 이제 Android 스튜디오에서 IntroductionToComposeForTV 프로젝트 폴더를 엽니다. 이제 시작할 준비가 되었습니다.

3. 카탈로그 브라우저 화면 구현

카탈로그 브라우저 화면을 통해 사용자는 영화 카탈로그를 둘러볼 수 있습니다. 카탈로그 브라우저를 컴포저블 함수로 구현합니다. CatalogBrowser.kt 파일에서 CatalogBrowser 컴포저블 함수를 찾을 수 있습니다. 이 컴포저블 함수에서 카탈로그 브라우저 화면을 구현합니다.

시작 코드에는 CatalogBrowserViewModel 클래스라는 ViewModel이 있으며, 이 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 객체에는 카테고리 이름을 나타내는 String 값인 name 속성이 있습니다.

카테고리 이름을 표시하려면 다음 단계를 따르세요.

  1. Android 스튜디오에서 시작 코드의 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. 람다에서 items 함수를 호출하며 이때 category.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 객체에는 영화의 메타데이터로 다음과 같은 세 문자열 속성이 있습니다.

  • title. 영화 제목
  • studio. 영화를 제작한 스튜디오의 이름
  • description. 영화에 대한 간단한 요약

이 메타데이터를 세부정보 화면에 표시하려면 다음 단계를 따르세요.

  1. Column 컴포저블 함수를 추가한 후에 Modifier.padding 메서드로 만든 Modifier 객체를 사용하여 열 주위에 세로 간격 32dp 및 가로 간격 48dp를 설정합니다.
  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 컴포저블 함수를 Details 컴포저블 함수를 통해 전달된 modifier 객체가 있는 Column 컴포저블 함수의 래퍼로 추가합니다.
  2. Box 컴포저블 함수에서 modifier 객체의 fillMaxSize 메서드를 호출하여 Details 컴포저블 함수에 할당 가능한 최대 크기를 Box 컴포저블 함수가 채우도록 합니다.
  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 객체에는 TypographyColorScheme 클래스의 함수와 같이 현재 테마 값을 참조하는 함수가 포함되어 있습니다.

일관된 테마 설정을 위해 MaterialTheme 객체를 참조하려면 다음 단계를 따르세요.

  1. MaterialTheme.typography.displayMedium 속성을 영화 제목의 텍스트 스타일로 설정합니다.
  2. MaterialTheme.typography.bodySmall 속성을 두 번째 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. Box 컴포저블 함수의 백그라운드를 background 수정자를 사용하여 설정하여 백그라운드를 MaterialTheme.colorScheme.background 값과 Color.Transparent가 포함된 Color 객체 목록으로 Brush.linearGradient 함수를 호출하여 만든 선형 그래디언트로 채웁니다.
  3. padding 수정자를 사용하여 Column 컴포저블 함수 주위에 48.dp 가로 및 24.dp 세로 간격을 설정합니다.
  4. 0.5f 값으로 Modifier.width 함수를 호출하여 만든 width 수정자를 사용하여 Column 컴포저블 함수의 너비를 설정합니다.
  5. Spacer를 사용하여 두 번째 Text 컴포저블 함수와 세 번째 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를 선언한 후에 State 객체를 위임되도록 설정합니다. 이는 catalogBrowserViewModel.featuredMovieList 속성에서 수집됩니다.
  4. item 함수 내에서 Carousel 컴포저블 함수를 호출한 후에 다음 매개변수를 전달합니다.
  • slideCount 매개변수를 통한 featuredMovieList 변수의 크기
  • Modifier.fillMaxWidth 메서드 및 Modifier.height 메서드를 사용하여 캐러셀 크기를 지정하는 Modifier 객체. Carousel 컴포저블 함수는 Modifier.height 메서드에 376.dp 값을 전달하여 높이 376dp를 사용합니다.
  • 표시된 캐러셀 항목의 색인을 나타내는 정수 값으로 호출된 람다
  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 컴포저블 함수를 호출하여 Movie 객체와 연결된 배경 이미지를 Text 컴포저블 함수의 백그라운드에 로드합니다.
  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. Button 컴포저블 함수의 onClick 매개변수에 전달된 람다에서 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. Column 컴포저블 함수를 Box 컴포저블로 래핑합니다.
  3. Alignment.BottomStart 값을 Box 구성요소의 contentAlignment 매개변수에 전달합니다.
  4. fillMaxSize 수정자를 Box 컴포저블 함수의 수정자 매개변수에 전달합니다. fillMaxSize 수정자는 Modifier.fillMaxSize() 함수로 만들어집니다.
  5. Box 컴포저블에 전달된 fillMaxSize 수정자를 통해 drawBehind() 메서드를 호출합니다.
  6. drawBehind 수정자에 전달된 람다에서 2개의 Color 객체 목록으로 Brush.linearGradient 함수를 호출하여 만든 Brush 객체로 brush 값을 할당합니다. backgroundColor 값과 Color.Transparent 값으로 listOf 함수를 호출하여 목록을 만듭니다.
  7. drawBehind 수정자에 전달된 람다에서 brush 객체로 drawRect를 호출하여 배경 이미지 위에 srim 레이어를 만듭니다.
  8. 20.dp 값으로 Modifier.padding을 호출하여 만든 padding 수정자를 사용하여 Column 컴포저블 함수의 패딩을 지정합니다.
  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 스튜디오에서 엽니다.

  • Git을 사용하여 가져옵니다.
$ git clone https://github.com/android/tv-codelabs.git
$ cd tv-codelabs
$ git checkout solution
$ cd IntroductionToComposeForTV

8. 축하합니다.

축하합니다. 다음과 같은 TV용 Compose의 기본사항을 살펴보았습니다.

  • LazyColumn과 LazyLow를 결합하여 콘텐츠 목록을 표시하도록 화면을 구현하는 방법
  • 콘텐츠 세부정보를 표시하는 기본 화면 구현
  • 두 화면 간의 화면 전환을 추가하는 방법