Giới thiệu về Compose dành cho TV

1. Trước khi bắt đầu

Compose cho TV là khung giao diện người dùng mới nhất để phát triển các ứng dụng chạy trên Android TV. Điều này giúp tận dụng toàn bộ lợi ích của Jetpack Compose cho các ứng dụng truyền hình, giúp bạn dễ dàng xây dựng giao diện người dùng đẹp có đầy đủ chức năng. Sau đây là một số lợi ích cụ thể của Compose dành cho TV:

  • Tính linh hoạt. Bạn có thể dùng Compose để tạo mọi loại giao diện người dùng với bố cục từ đơn giản cho đến có ảnh động phức tạp. Các thành phần vẫn hoạt động tốt, nhưng cũng có thể được tuỳ chỉnh và định kiểu cho phù hợp với nhu cầu của ứng dụng.
  • Quá trình phát triển nhanh chóng và đơn giản. Compose tương thích với mã nguồn hiện có và cho phép nhà phát triển tạo ứng dụng với ít mã hơn.
  • Tính trực quan: Compose sử dụng cú pháp khai báo giúp việc thay đổi giao diện người dùng, gỡ lỗi, hiểu và xem lại mã trở nên trực quan.

Một trường hợp sử dụng phổ biến đối với ứng dụng truyền hình là tiêu thụ nội dung phương tiện. Người dùng duyệt qua danh mục nội dung và chọn nội dung họ muốn xem. Nội dung có thể là phim, chương trình truyền hình hoặc podcast. Sau khi chọn một nội dung, có thể người dùng muốn xem thêm thông tin về nội dung đó, chẳng hạn như đoạn mô tả ngắn, thời lượng phát và tên của nhà sản xuất. Trong lớp học lập trình này, bạn sẽ tìm hiểu cách triển khai màn hình trình duyệt danh mục và màn hình hiển thị thông tin chi tiết trong Compose dành cho TV.

Điều kiện tiên quyết

  • Kinh nghiệm về cú pháp Kotlin, bao gồm cả lambda.
  • Kinh nghiệm cơ bản về Compose Nếu bạn chưa hiểu rõ về Compose, hãy hoàn thành lớp học lập trình Kiến thức cơ bản về Jetpack Compose.
  • Kiến thức cơ bản về thành phần kết hợp và đối tượng sửa đổi.

Sản phẩm bạn sẽ tạo ra

  • Ứng dụng phát video có màn hình duyệt danh mục và màn hình hiển thị thông tin chi tiết.
  • Màn hình duyệt danh mục hiển thị danh sách video để người dùng chọn. Màn sẽ có giao diện như hình sau:

Màn duyệt danh mục hiển thị danh sách phim nổi bật\nvới một băng chuyền ở phía trên cùng.\nMàn hình cũng hiển thị một danh sách phim cho mỗi danh mục.

  • Màn hình chi tiết hiển thị siêu dữ liệu của video đã chọn, chẳng hạn như tên, đoạn mô tả và thời lượng. Màn sẽ có giao diện như hình sau:

Màn hình chi tiết cho thấy siêu dữ liệu của phim,\ntrong đó có tên phim, xưởng phim và phần mô tả ngắn.\nSiêu dữ liệu xuất hiện trên hình nền liên kết với phim.

Bạn cần có

2. Bắt đầu thiết lập

Để lấy đoạn mã chứa giao diện và và chế độ thiết lập cơ bản cho lớp học lập trình này, hãy làm theo một trong những cách sau:

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

Nhánh main chứa mã nguồn khởi đầu và nhánh solution chứa mã nguồn giải pháp.

  • Tải tệp main.zip chứa mã nguồn khởi đầu và tệp solution.zip chứa mã nguồn giải pháp.

Sau khi tải mã nguồn xuống, bạn hãy mở thư mục dự án IntroductionToComposeForTV trong Android Studio. Bây giờ, bạn đã sẵn sàng để bắt đầu.

3. Triển khai màn hình duyệt danh mục trình duyệt

Màn hình duyệt danh mục cho phép người dùng duyệt xem danh mục phim. Bạn triển khai màn duyệt danh mục dưới dạng hàm Composable. Bạn có thể tìm thấy hàm CatalogBrowser Composable trong tệp CatalogBrowser.kt. Bạn triển khai màn hình duyệt danh mục trong hàm Composable này.

Mã khởi đầu có ViewModel gọi tới lớp CatalogBrowserViewModel. Lớp này có một số thuộc tính và phương thức để truy xuất các đối tượng Movie mô tả nội dung phim. Bạn triển khai một màn duyệt danh mục có các đối tượng Movie đã truy xuất.

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 = {}
) {
}

Hiển thị tên danh mục

Bạn có thể truy cập vào danh sách danh mục bằng thuộc tính catalogBrowserViewModel.categoryList (là một luồng của danh sách Category). Luồng (flow) này được thu thập dưới dạng đối tượng State trong Compose bằng cách gọi phương thức collectAsState. Đối tượng Category có thuộc tính name (là giá trị String biểu diễn tên danh mục).

Để hiển thị tên danh mục, hãy làm theo các bước sau:

  1. Trong Android Studio, hãy mở tệp CatalogBrowser.kt của mã nguồn khởi đầu, sau đó thêm hàm TvLazyColumn Composable vào hàm CatalogBrowser Composable.
  2. Gọi phương thức catalogBrowserViewModel.categoryList.collectAsState() để thu thập luồng dưới dạng đối tượng State.
  3. Khai báo categoryList dưới dạng thuộc tính uỷ quyền của đối tượng State mà bạn đã tạo ở bước trước.
  4. Gọi hàm items có biến categoryList làm tham số.
  5. Gọi hàm Text Composable bằng tên danh mục làm tham số được truyền dưới dạng đối số của lambda.

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)
        }
    }
}

Hiển thị danh sách nội dung cho từng danh mục

Đối tượng Category có một thuộc tính khác có tên là movieList. Thuộc tính này là danh sách các đối tượng Movie biểu diễn cho phim thuộc danh mục đó.

Để hiển thị danh sách nội dung cho từng danh mục, hãy làm theo các bước sau:

  1. Thêm hàm TvLazyRow Composable, sau đó truyền hàm lambda vào hàm đó.
  2. Trong hàm lambda, hãy gọi hàm items bằng category.movieList rồi truyền biểu thức lambda vào thuộc tính đó.
  3. Trong hàm lambda được truyền vào hàm items, hãy gọi hàm MovieCard Composable bằng đối tượng Movie.

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)
                }
            }
        }
    }
}

Điều chỉnh bố cục (không bắt buộc)

  1. Để đặt khoảng cách giữa các danh mục, hãy truyền đối tượng Arrangement vào hàm TvLazyColumn Composable bằng tham số verticalArrangement. Đối tượng Arrangement được tạo bằng cách gọi phương thức Arrangement#spacedBy.
  2. Để đặt khoảng cách giữa các thẻ phim, hãy truyền một đối tượng Arrangement vào hàm TvLazyRow Composable bằng tham số horizontalArrangement.
  3. Để đặt khoảng thụt lề cho cột này, hãy truyền đối tượng PaddingValue có tham số contentPadding.

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. Triển khai màn hình chi tiết

Màn hình chi tiết hiển thị thông tin về bộ phim đã chọn. Có một hàm Details Composable trong tệp Details.kt. Bạn thêm mã vào hàm này để triển khai màn hình chi tiết.

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) {
}

Hiển thị tên phim, tên xưởng phim và nội dung mô tả

Đối tượng Movie có ba thuộc tính chuỗi dưới dạng siêu dữ liệu của phim:

  • title. Tên phim.
  • studio. Tên của studio sản xuất phim.
  • description. Tóm tắt ngắn về phim.

Để hiển thị siêu dữ liệu này trên màn hình chi tiết, hãy làm theo các bước sau:

  1. Thêm một hàm Column Composable, sau đó đặt khoảng trống có kích thước 32 dp theo chiều dọc và 48 dp theo chiều ngang xung quanh cột bằng đối tượng Modifier được tạo bằng phương thức Modifier.padding.
  2. Thêm một hàm Text Composable để hiển thị tên phim.
  3. Thêm một hàm Text Composable để hiển thị tên studio.
  4. Thêm hàm Text Composable để hiển thị nội dung mô tả phim.

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)
    }
}

Đối tượng Modifier được chỉ định trong tham số của hàm Details Composable được dùng trong tác vụ tiếp theo.

Hiển thị hình nền được liên kết với một đối tượng Movie nhất định

Đối tượng Movie có thuộc tính backgroundImageUrl cho biết vị trí của hình nền cho phim mà đối tượng mô tả.

Để hiển thị hình nền cho một bộ phim cụ thể, hãy làm theo các bước sau:

  1. Thêm một hàm Box Composable để gói Column Composable có đối tượng modifier được truyền qua hàm Details Composable.
  2. Trong hàm Box Composable, hãy gọi phương thức fillMaxSize của đối tượng modifier để hàm Box Composable lấp đầy kích thước tối đa có thể được phân bổ cho Details Composable.
  3. Thêm một hàm AsyncImage Composable có các tham số sau vào hàm Box Composable:
  • Đặt giá trị của thuộc tính backgroundImageUrl của đối tượng Movie đã cho thành tham số model.
  • Truyền giá trị null vào tham số contentDescription.
  • Truyền một đối tượng ContentScale.Crop đến tham số contentScale. Để xem các lựa chọn cho ContentScale, hãy xem phần Phạm vi của nội dung.
  • Truyền giá trị trả về của phương thức Modifier.fillMaxSize vào tham số modifier.
  • Đặt khoảng trống có kích thước 32 dp theo chiều dọc và 48 dp theo chiều ngang so với cột bằng cách đặt đối tượng Modifier được tạo bằng cách gọi phương thức Modifier.padding.

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,
            )
        }
    }
}

Tham khảo đối tượng MaterialTheme để tạo giao diện nhất quán

Đối tượng MaterialTheme chứa các hàm tham chiếu đến các giá trị giao diện hiện tại, chẳng hạn như các giá trị trong các lớp Typography và [ColorScheme][ColorScheme].

Để tham chiếu đến đối tượng MaterialTheme nhằm tạo giao diện nhất quán, hãy làm theo các bước sau:

  1. Đặt thuộc tính MaterialTheme.typography.headlineLarge thành kiểu văn bản của tên phim.
  2. Đặt thuộc tính MaterialTheme.typography.headlineMedium thành kiểu văn bản của hai hàm Text Composable nữa.
  3. Đặt thuộc tính MaterialTheme.colorScheme.background thành màu nền của hàm Column Composable bằng phương thức Modifier.background.

[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. Thêm tính năng điều hướng giữa các màn hình

Giờ đây, bạn đã có màn hình duyệt danh mục và màn hình chi tiết. Sau khi người dùng chọn nội dung trên màn hình duyệt danh mục, màn hình đó phải chuyển sang màn hình chi tiết. Để có thể thực hiện việc này, bạn hãy sử dụng đối tượng sửa đổi clickable để thêm trình nghe event vào hàm MovieCard Composable. Khi nhấn nút giữa của bàn phím di chuyển, phương thức CatalogBrowserViewModel#showDetails sẽ được gọi với đối tượng phim liên kết với hàm MovieCard Composable làm đối số.

  1. Mở tệp com.example.tvcomposeintroduction.ui.screens.CatalogBrowser.
  2. Truyền một hàm lambda vào hàm MovieCard Composable có tham số onClick.
  3. Gọi lệnh gọi lại onMovieSelected với đối tượng phim liên kết với hàm MovieCard Composable.

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. Thêm băng chuyền vào màn hình duyệt danh mục để làm nổi bật phần nội dung nổi bật

Băng chuyền là thành phần giao diện người dùng thường được điều chỉnh. Thành phần này sẽ tự động cập nhật trang trình bày sau một khoảng thời gian cụ thể. Thẻ này thường được dùng để làm nổi bật phần nội dung nổi bật.

Để thêm băng chuyền vào màn hình trình duyệt danh mục để làm nổi bật phim trong danh sách nội dung nổi bật, hãy làm theo các bước sau:

  1. Mở tệp com.example.tvcomposeintroduction.ui.screens.CatalogBrowser.
  2. Gọi hàm item để thêm một mục vào hàm TvLazyColumn Composable.
  3. Khai báo featuredMovieList dưới dạng một thuộc tính được uỷ quyền trong hàm lambda được truyền đến hàm item rồi đặt đối tượng State thành được uỷ quyền. Hàm này được thu thập từ thuộc tính catalogBrowserViewModel.featuredMovieList.
  4. Gọi hàm Carousel Composable bên trong hàm item, sau đó truyền các tham số sau:
  • Kích thước của biến featuredMovieList thông qua tham số slideCount.
  • Đối tượng Modifier để chỉ định kích thước băng chuyền bằng phương thức Modifier.fillMaxWidthModifier.height. Hàm Carousel Composable sử dụng chiều cao có giá trị 376 dp bằng cách truyền giá trị 376.dp vào phương thức Modifier.height.
  • Hàm lambda được gọi với một giá trị số nguyên cho biết chỉ mục của mục băng chuyền đang hiển thị.
  1. Truy xuất đối tượng Movie từ biến featuredMovieList và giá trị chỉ mục đã cho.
  2. Thêm hàm CarouselSlide Composable vào hàm Carousel Composable.
  3. Thêm hàm Text Composable vào hàm CarouselSlide Composable để hiển thị tên phim.

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) })
                }
            }
        }
    }
}

Hiển thị hình nền

Hàm CarouselSlide Composable có thể lấy một hàm lambda khác để chỉ định cách nền của hàm CarouselSlide Composable hiển thị.

Để hiển thị hình nền, hãy làm theo các bước sau:

  1. Truyền một hàm lambda vào hàm CarouselSlide Composable bằng tham số background.
  2. Gọi hàm AsyncImage Composable để tải hình nền liên kết với đối tượng Movie làm nền của hàm CarouselSlide Composable.
  3. Cập nhật vị trí và kiểu văn bản của hàm Text Composable trong hàm CarouselSlide Composable để hiển thị tốt hơn.
  4. Đặt một phần giữ chỗ cho hàm AsyncImage Composable để tránh xáo trộn bố cục. Mã khởi đầu có phần giữ chỗ dưới dạng một đối tượng có thể vẽ mà bạn có thể tham chiếu bằng 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) })
                }
            }
        }
    }
}

Thêm hiệu ứng chuyển đổi sang màn hình chi tiết

Bạn có thể cho phép người dùng nhấp vào hàm CarouselSlide Composable.

Để cho phép người dùng xem thông tin về phim trong phần băng chuyền hiển thị trên màn hình chi tiết, hãy làm theo các bước sau:

  1. Truyền giá trị trả về của phương thức Modifier.clickable vào hàm CarouselSlide Composable thông qua tham số modifier.
  2. Gọi hàm onMovieSelected có đối tượng Movie cho hàm CarouselSlide Composable hiển thị trong lambda được truyền vào phương thức Modifier.clickable.

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. Lấy mã giải pháp

Để tải mã giải pháp cho lớp học lập trình này, hãy làm theo một trong những cách sau:

  • Nhấp vào nút sau để tải tệp xuống dưới dạng tệp zip, sau đó giải nén và mở tệp trong Android Studio.

  • Truy xuất bằng Git:
$ git clone https://github.com/android/tv-codelabs.git
$ cd tv-codelabs
$ git checkout solution
$ cd IntroductionToComposeForTV

8. Chúc mừng bạn!

Xin chúc mừng! Bạn đã tìm hiểu các kiến thức cơ bản về Compose dành cho TV!

  • Cách triển khai màn hình để hiển thị danh sách nội dung bằng cách kết hợp TvLazyColumn và TvLazyLow.
  • Triển khai màn hình cơ bản để hiển thị nội dung chi tiết.
  • Cách thêm hiệu ứng chuyển đổi giữa hai màn hình.