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.
What Is an Interactive Flutter Video Grid?
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.
Features of Our Interactive Video Grid:
- Videos are displayed in a neat grid layout.
- Users can play a video by tapping or hovering on it.
- When a video finishes, a placeholder image is shown.
- Only one video plays at a time to conserve resources and maintain clarity.
Implementation
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.
1. Define the Video Item Model
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}); } |
2. Create the Video Grid Page
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]); }, ), ), ); } } |
3. Implement the Video Grid Item
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, ), ); }, ); } } |
4. VideoCard Widget
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), ], ), ), ); } } |
5. VideoThumbnail Widget
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, ), ), ], ), ); } } |
6. VideoDetails Widget
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, ), ], ), ); } } |
7. Manage State with Bloc
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); } } |
Output
Below is the output of the complete example code.
Interactive Flutter Video Grid
Conclusion
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…