image

06 Feb 2026

9K

35K

Building a Movie Detail Page Widget with a Trailer Player in Flutter

Creating engaging user interfaces is paramount in modern mobile applications. For media-rich apps like movie streamers or catalogs, a well-designed Movie Detail Page is crucial. This page not only presents essential information about a film but also enhances the user experience by providing a direct way to view trailers. In this article, we will walk through building a dynamic Movie Detail Page widget in Flutter, complete with a trailer player.

Prerequisites and Dependencies

Before diving into the code, ensure you have Flutter installed and set up. We'll need a few packages for network requests and YouTube video playback. Add the following to your pubspec.yaml file:


dependencies:
  flutter:
    sdk: flutter
  http: ^1.1.0 # For making HTTP requests
  youtube_player_flutter: ^8.1.0 # For playing YouTube videos
  cached_network_image: ^3.3.0 # Optional: For efficient image caching

After updating pubspec.yaml, run flutter pub get.

1. Movie Data Model

First, let's define a simple data model for our movie. This model will parse the movie details received from an API.


// lib/models/movie.dart
class Movie {
  final int id;
  final String title;
  final String overview;
  final String posterPath;
  final double voteAverage;
  final String? youtubeKey; // Added for trailer

  Movie({
    required this.id,
    required this.title,
    required this.overview,
    required this.posterPath,
    required this.voteAverage,
    this.youtubeKey,
  });

  factory Movie.fromJson(Map<String, dynamic> json) {
    return Movie(
      id: json['id'],
      title: json['title'],
      overview: json['overview'],
      posterPath: json['poster_path'],
      voteAverage: (json['vote_average'] as num).toDouble(),
      youtubeKey: null, // Will be fetched separately or populated later
    );
  }

  // Method to create a copy with a new youtubeKey
  Movie copyWith({String? youtubeKey}) {
    return Movie(
      id: this.id,
      title: this.title,
      overview: this.overview,
      posterPath: this.posterPath,
      voteAverage: this.voteAverage,
      youtubeKey: youtubeKey ?? this.youtubeKey,
    );
  }
}

2. API Service for Fetching Movie Details and Trailer

We'll create a simple API service to fetch movie details and its trailer. For demonstration purposes, we'll assume an API that provides movie details and a separate endpoint for videos. You'll need to replace YOUR_API_KEY and potentially the base URLs with a real API like TMDB.


// lib/services/api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/movie.dart';

class ApiService {
  static const String _apiKey = 'YOUR_API_KEY'; // Replace with your actual API key
  static const String _baseUrl = 'https://api.themoviedb.org/3'; // Example TMDB base URL
  static const String _baseImageUrl = 'https://image.tmdb.org/t/p/w500';

  Future<Movie> fetchMovieDetail(int movieId) async {
    final response = await http.get(Uri.parse('$_baseUrl/movie/$movieId?api_key=$_apiKey'));

    if (response.statusCode == 200) {
      final movieJson = json.decode(response.body);
      Movie movie = Movie.fromJson(movieJson);

      // Fetch trailer key
      final videoResponse = await http.get(Uri.parse('$_baseUrl/movie/$movieId/videos?api_key=$_apiKey'));
      if (videoResponse.statusCode == 200) {
        final videoJson = json.decode(videoResponse.body);
        final List videos = videoJson['results'];
        // Find the first YouTube trailer
        final trailer = videos.firstWhere(
          (video) => video['site'] == 'YouTube' && video['type'] == 'Trailer',
          orElse: () => null,
        );
        if (trailer != null) {
          movie = movie.copyWith(youtubeKey: trailer['key']);
        }
      }
      return movie;
    } else {
      throw Exception('Failed to load movie details');
    }
  }

  String getPosterUrl(String path) {
    return '$_baseImageUrl$path';
  }
}

3. Building the Movie Detail Page Widget

The MovieDetailPage will be a StatefulWidget to manage the future data and the YouTube player controller. We will use a FutureBuilder to handle the asynchronous fetching of movie details.


// lib/pages/movie_detail_page.dart
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
import '../models/movie.dart';
import '../services/api_service.dart';

class MovieDetailPage extends StatefulWidget {
  final int movieId;

  const MovieDetailPage({Key? key, required this.movieId}) : super(key: key);

  @override
  State<MovieDetailPage> createState() => _MovieDetailPageState();
}

class _MovieDetailPageState extends State<MovieDetailPage> {
  late Future<Movie> _movieDetail;
  YoutubePlayerController? _youtubeController;

  @override
  void initState() {
    super.initState();
    _movieDetail = ApiService().fetchMovieDetail(widget.movieId);
  }

  @override
  void dispose() {
    _youtubeController?.dispose();
    super.dispose();
  }

  void _initializeYoutubePlayer(String youtubeKey) {
    _youtubeController = YoutubePlayerController(
      initialVideoId: youtubeKey,
      flags: const YoutubePlayerFlags(
        autoPlay: false,
        mute: false,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Movie Details'),
        backgroundColor: Colors.black,
      ),
      body: FutureBuilder<Movie>(
        future: _movieDetail,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          } else if (snapshot.hasError) {
            return Center(child: Text('Error: ${snapshot.error}'));
          } else if (snapshot.hasData) {
            final movie = snapshot.data!;
            if (movie.youtubeKey != null && _youtubeController == null) {
              _initializeYoutubePlayer(movie.youtubeKey!);
            }
            return SingleChildScrollView(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Movie Poster and Trailer Section
                  if (_youtubeController != null)
                    YoutubePlayer(
                      controller: _youtubeController!,
                      showVideoProgressIndicator: true,
                      progressIndicatorColor: Colors.amber,
                      progressColors: const ProgressBarColors(
                        playedColor: Colors.amber,
                        handleColor: Colors.amberAccent,
                      ),
                    )
                  else
                    AspectRatio(
                      aspectRatio: 16 / 9, // Example aspect ratio
                      child: movie.posterPath.isNotEmpty
                          ? CachedNetworkImage(
                              imageUrl: ApiService().getPosterUrl(movie.posterPath),
                              fit: BoxFit.cover,
                              placeholder: (context, url) => const Center(child: CircularProgressIndicator()),
                              errorWidget: (context, url, error) => const Icon(Icons.error),
                            )
                          : const Center(child: Text('No Poster Available')),
                    ),

                  Padding(
                    padding: const EdgeInsets.all(16.0),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          movie.title,
                          style: const TextStyle(
                            fontSize: 28,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 8),
                        Row(
                          children: [
                            const Icon(Icons.star, color: Colors.amber, size: 20),
                            const SizedBox(width: 4),
                            Text(
                              '${movie.voteAverage.toStringAsFixed(1)}/10',
                              style: const TextStyle(fontSize: 16),
                            ),
                          ],
                        ),
                        const SizedBox(height: 16),
                        const Text(
                          'Overview:',
                          style: TextStyle(
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 8),
                        Text(
                          movie.overview,
                          style: const TextStyle(fontSize: 16, height: 1.5),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            );
          }
          return const SizedBox.shrink(); // Should not happen
        },
      ),
    );
  }
}

4. Integrating into Your Application

To display the MovieDetailPage, you can navigate to it from another screen, for example, a list of movies. Here's how you might push it using Navigator:


// Example of how to use it
// In a StatefulWidget or StatelessWidget that has a BuildContext
// Say you have a movie ID from a list
int selectedMovieId = 550; // Example: Fight Club ID

Navigator.push(
  context,
  MaterialPageRoute(
    builder: (context) => MovieDetailPage(movieId: selectedMovieId),
  ),
);

Explanation of Key Components

  • FutureBuilder<Movie>: This widget is essential for handling asynchronous data. It listens to the state of a Future (in our case, _movieDetail) and rebuilds its UI based on whether the data is loading (ConnectionState.waiting), has an error (snapshot.hasError), or has successfully loaded (snapshot.hasData).
  • ApiService: Encapsulates network logic, making it cleaner to fetch data and construct image URLs.
  • youtube_player_flutter: The YoutubePlayer widget takes a YoutubePlayerController. This controller is initialized with the YouTube video ID (youtubeKey) fetched from the API. The player is conditionally rendered: if a trailer key is available, the player is shown; otherwise, the movie poster is displayed.
  • CachedNetworkImage: (Optional but recommended) This package efficiently loads and caches network images, improving performance and user experience by preventing repeated downloads.
  • UI Layout: A SingleChildScrollView wraps the content to prevent overflow, especially on smaller screens or with long movie overviews. A Column organizes the poster/trailer, title, rating, and overview vertically.

Conclusion

We've successfully built a dynamic Movie Detail Page in Flutter, capable of displaying detailed movie information and playing an embedded YouTube trailer. This setup provides a robust foundation, allowing for further enhancements such as adding cast and crew details, similar movies, user reviews, or a "favorite" button. By leveraging Flutter's reactive framework and powerful third-party packages, you can create rich and interactive media applications with relative ease.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is