What You Will Build

A movie catalog with horizontal card layouts showing poster placeholders, titles, release years, star ratings, and a call-to-action button. This is the standard pattern used by IMDB, Letterboxd, and streaming app browse screens.

Why This Pattern Matters

Horizontal list items with image + text + actions are the most common card pattern in Android apps. Building this teaches you Row-based layouts inside LazyColumn items, star rating rendering with repeat(), and gradient placeholder images.

Step 1: Define the Movie Model

data class Movie(
    val title: String,
    val year: String,
    val rating: Float
)

val movies = listOf(
    Movie("The Matrix", "1999", 4.8f),
    Movie("Inception", "2010", 4.7f),
    Movie("Interstellar", "2014", 4.9f),
    Movie("Dune", "2021", 4.5f),
    Movie("Oppenheimer", "2023", 4.6f)
)

Step 2: Build the Movie Card

@Composable
fun MovieCard(movie: Movie) {
    Card(Modifier.fillMaxWidth()) {
        Row(Modifier.padding(12.dp)) {
            // Poster placeholder with gradient
            Box(
                Modifier.size(100.dp, 140.dp)
                    .clip(RoundedCornerShape(8.dp))
                    .background(
                        Brush.linearGradient(
                            listOf(Color(0xFF6C63FF), Color(0xFFFF6584))
                        )
                    ),
                contentAlignment = Alignment.Center
            ) {
                Icon(Icons.Default.Movie, null,
                    tint = Color.White, modifier = Modifier.size(36.dp))
            }
            Spacer(Modifier.width(16.dp))
            Column {
                Text(movie.title, fontWeight = FontWeight.Bold, fontSize = 18.sp)
                Text(movie.year,
                    color = MaterialTheme.colorScheme.onSurfaceVariant)
                Spacer(Modifier.height(8.dp))
                // Star rating
                Row {
                    repeat(5) { i ->
                        Icon(
                            if (i < movie.rating.toInt()) Icons.Default.Star
                            else Icons.Default.StarBorder,
                            null,
                            modifier = Modifier.size(16.dp),
                            tint = Color(0xFFFFD700)
                        )
                    }
                }
                Spacer(Modifier.height(8.dp))
                Button(onClick = {}, modifier = Modifier.height(36.dp)) {
                    Text("Watch Now", fontSize = 13.sp)
                }
            }
        }
    }
}

Step 3: Assemble in a LazyColumn

Scaffold(topBar = { TopAppBar(title = { Text("Movie App") }) }) { padding ->
    LazyColumn(
        Modifier.padding(padding).padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(12.dp)
    ) {
        items(movies) { movie -> MovieCard(movie) }
    }
}

Tips and Pitfalls

  • repeat(5) for star ratings is cleaner than creating 5 separate Icon composables. Compare the index against rating.toInt() to choose filled vs outlined stars.
  • Fixed poster dimensions (100x140) maintain a consistent 5:7 aspect ratio matching real movie posters.
  • Gradient placeholders look professional while you wire up actual image loading with Coil or Glide.
  • Extract MovieCard as a composable to keep the LazyColumn items block clean and enable preview.

Min SDK: 21 | Compose BOM: 2024.01.00+

Get New Tutorials by Email

No spam. Just clear, practical breakdowns you can apply right away.

Enjoy this tutorial?

Get new practical tech tutorials in your inbox.