Updated 23 January 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 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
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; bool _isLoading = true; @override void initState() { super.initState(); _initializeController(); } 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(); super.dispose(); } @override Widget build(BuildContext context) { return BlocBuilder<VideoCubit, int?>( builder: (context, playingIndex) { final isPlaying = playingIndex == widget.index; return GestureDetector( onPanDown: (_) { context.read<VideoCubit>().playVideo(widget.index); }, child: Card( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), elevation: 4, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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, ), ), ], ), ), Padding( padding: const EdgeInsets.all(8.0), child: Text( widget.videoItem.title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( widget.videoItem.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.