Updated 4 February 2025
Flutter has emerged as a powerful tool for building visually rich and responsive applications. One of its many capabilities is creating interactive media-rich components like a video grid.
In this article, we’ll dive into how to create an Interactive Video Grid in Flutter that responds to user interactions for seamless playback experiences.
An interactive video grid is a collection of videos arranged in a grid layout where users can interact with individual video items.
In this example, tapping or hovering over a video starts its playback, while others pause.
This dynamic interaction ensures that the interface is intuitive and engaging, especially for applications like media libraries, video tutorials, or e-commerce platforms showcasing product demos.
Add dependencies: To implement an Interactive Flutter Video Grid, you’ll need these packages. Add the following to your pubspec.yaml file:
1 2 |
video_player: ^2.9.2 flutter_bloc: ^9.0.0 |
Run flutter pub get
to fetch the dependencies.
First, we need a model to represent each video item:
1 2 3 4 5 6 7 |
class VideoItem { final String url; final String title; final String description; VideoItem({required this.url, required this.title, required this.description}); } |
The VideoGridPage
displays the grid of video items:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
class VideoGridPage extends StatelessWidget { final List<VideoItem> videoItems = List.generate( 1000, (index) => VideoItem( url: videoUrls[index % videoUrls.length], title: 'Video Title $index', description: 'This is a description for video $index.', ), ); static const List<String> videoUrls = [ 'https://www.w3schools.com/tags/mov_bbb.mp4', ]; @override Widget build(BuildContext context) { return BlocProvider( create: (_) => VideoCubit(), child: Scaffold( appBar: AppBar(title: const Text('Video Grid')), body: GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 8, mainAxisExtent: 300, // Consistent height for grid items ), itemCount: videoItems.length, itemBuilder: (context, index) { return VideoGridItem(index: index, videoItem: videoItems[index]); }, ), ), ); } } |
The VideoGridItem
widget handles the video playback and interaction:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
class VideoGridItem extends StatefulWidget { final int index; final VideoItem videoItem; VideoGridItem({required this.index, required this.videoItem}); @override _VideoGridItemState createState() => _VideoGridItemState(); } class _VideoGridItemState extends State<VideoGridItem> { VideoPlayerController? _controller; // Manages video playback. bool _isLoading = true; // Indicates whether the video is loading. @override void initState() { super.initState(); _initializeController(); } // Initializes the video controller and registers it with a Cubit. void _initializeController() { _controller = VideoPlayerController.network(widget.videoItem.url) ..initialize().then((_) { if (mounted) { context.read<VideoCubit>().registerController(widget.index, _controller!); setState(() { _isLoading = false; }); } }); } @override void dispose() { _controller?.dispose(); // Disposes of the video controller to free resources. super.dispose(); } @override Widget build(BuildContext context) { return BlocBuilder<VideoCubit, int?>( builder: (context, playingIndex) { final isPlaying = playingIndex == widget.index; // Checks if the current video is playing. return GestureDetector( onPanDown: (_) { context.read<VideoCubit>().playVideo(widget.index); // Triggers video playback. }, child: VideoCard( videoItem: widget.videoItem, controller: _controller, isPlaying: isPlaying, ), ); }, ); } } |
The VideoCard
widget encapsulates the entire card UI, delegating its parts to specialized sub-widgets.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
class VideoCard extends StatelessWidget { final VideoItem videoItem; final VideoPlayerController? controller; final bool isPlaying; const VideoCard({ required this.videoItem, this.controller, required this.isPlaying, }); @override Widget build(BuildContext context) { return Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 4, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Displays either the video or a placeholder image. VideoThumbnail(controller: controller, isPlaying: isPlaying), // Displays the title and description of the video. VideoDetails(title: videoItem.title, description: videoItem.description), ], ), ), ); } } |
This widget displays either the video player or a placeholder image depending on whether the video is initialized and currently playing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
class VideoThumbnail extends StatelessWidget { final VideoPlayerController? controller; final bool isPlaying; const VideoThumbnail({ this.controller, required this.isPlaying, }); @override Widget build(BuildContext context) { return Expanded( child: Stack( alignment: Alignment.center, children: [ if (controller != null && controller!.value.isInitialized && isPlaying) ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: SizedBox( width: double.infinity, height: double.infinity, child: VideoPlayer(controller!), ), ) else ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: Image.network( 'https://demo.bagisto.com/mobikul-common/cache/large/product/257/zBQMHTNayuES5B5IyDhOJKdTqrcd0mQDERhAhGHE.webp', fit: BoxFit.cover, width: double.infinity, height: double.infinity, ), ), ], ), ); } } |
The VideoDetails
widget combines the title and description into a single widget for simplicity and better organization.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class VideoDetails extends StatelessWidget { final String title; final String description; const VideoDetails({ required this.title, required this.description, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), // Adds spacing between title and description. Text( description, style: const TextStyle( fontSize: 14, color: Colors.grey, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ], ), ); } } |
Using flutter_bloc
, we ensure only one video plays at a time:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
class VideoCubit extends Cubit<int?> { final Map<int, VideoPlayerController> _controllers = {}; VideoCubit() : super(null); void registerController(int index, VideoPlayerController controller) { _controllers[index] = controller; } void playVideo(int index) { for (var entry in _controllers.entries) { if (entry.key == index) { entry.value.play(); } else { entry.value.pause(); } } emit(index); } void stopVideo() { for (var controller in _controllers.values) { controller.pause(); } emit(null); } } |
Below is the output of the complete example code.
Thanks for reading this article ❤️
I hope this blog will help you learn about how to Implement an Interactive Flutter Video Grid and you will be able to implement it.
For more updates, make sure to keep following Mobikul Blogs to learn more about mobile app development.
Happy Learning ✍️
Other blogs you may like…
If you have more details or questions, you can reply to the received confirmation email.
Back to Home
Be the first to comment.