Back to Posts

Flutter Media Widgets: Working with Images, Video, and Audio

24 min read

Media widgets are essential for creating engaging user experiences in Flutter applications. This comprehensive guide covers how to work with images, video, and audio content effectively.

1. Image Widgets

Basic Image Loading

Flutter provides multiple ways to load and display images.

class ImageExamples extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // Network Image
        Image.network(
          'https://example.com/image.jpg',
          width: 200,
          height: 200,
          fit: BoxFit.cover,
          loadingBuilder: (context, child, loadingProgress) {
            if (loadingProgress == null) return child;
            return Center(
              child: CircularProgressIndicator(
                value: loadingProgress.expectedTotalBytes != null
                    ? loadingProgress.cumulativeBytesLoaded /
                        loadingProgress.expectedTotalBytes!
                    : null,
              ),
            );
          },
          errorBuilder: (context, error, stackTrace) {
            return Container(
              width: 200,
              height: 200,
              color: Colors.grey[300],
              child: Icon(Icons.error),
            );
          },
        ),
        
        SizedBox(height: 16),
        
        // Asset Image
        Image.asset(
          'assets/images/local_image.png',
          width: 200,
          height: 200,
          fit: BoxFit.contain,
        ),
      ],
    );
  }
}

Advanced Image Handling

Implement caching and placeholder strategies.

class CachedImageExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CachedNetworkImage(
      imageUrl: 'https://example.com/image.jpg',
      imageBuilder: (context, imageProvider) => Container(
        decoration: BoxDecoration(
          image: DecorationImage(
            image: imageProvider,
            fit: BoxFit.cover,
            colorFilter: ColorFilter.mode(
              Colors.blue.withOpacity(0.2),
              BlendMode.colorBurn,
            ),
          ),
          borderRadius: BorderRadius.circular(16),
          boxShadow: [
            BoxShadow(
              color: Colors.black.withOpacity(0.1),
              blurRadius: 8,
              offset: Offset(0, 4),
            ),
          ],
        ),
      ),
      placeholder: (context, url) => ShimmerPlaceholder(),
      errorWidget: (context, url, error) => ErrorPlaceholder(),
    );
  }
}

class ShimmerPlaceholder extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Shimmer.fromColors(
      baseColor: Colors.grey[300]!,
      highlightColor: Colors.grey[100]!,
      child: Container(
        width: 200,
        height: 200,
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(16),
        ),
      ),
    );
  }
}

2. Video Widgets

Video Player Implementation

Implement video playback with controls.

class VideoPlayerExample extends StatefulWidget {
  @override
  _VideoPlayerExampleState createState() => _VideoPlayerExampleState();
}

class _VideoPlayerExampleState extends State<VideoPlayerExample> {
  late VideoPlayerController _controller;
  late Future<void> _initializeVideoPlayerFuture;

  @override
  void initState() {
    super.initState();
    _controller = VideoPlayerController.network(
      'https://example.com/video.mp4',
    );
    _initializeVideoPlayerFuture = _controller.initialize();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        FutureBuilder(
          future: _initializeVideoPlayerFuture,
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.done) {
              return AspectRatio(
                aspectRatio: _controller.value.aspectRatio,
                child: Stack(
                  alignment: Alignment.bottomCenter,
                  children: [
                    VideoPlayer(_controller),
                    VideoProgressIndicator(
                      _controller,
                      allowScrubbing: true,
                      colors: VideoProgressColors(
                        playedColor: Colors.blue,
                        bufferedColor: Colors.grey,
                      ),
                    ),
                    _buildControls(),
                  ],
                ),
              );
            } else {
              return Center(child: CircularProgressIndicator());
            }
          },
        ),
      ],
    );
  }

  Widget _buildControls() {
    return Container(
      color: Colors.black26,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          IconButton(
            icon: Icon(
              _controller.value.isPlaying
                  ? Icons.pause
                  : Icons.play_arrow,
              color: Colors.white,
            ),
            onPressed: () {
              setState(() {
                _controller.value.isPlaying
                    ? _controller.pause()
                    : _controller.play();
              });
            },
          ),
          IconButton(
            icon: Icon(
              _controller.value.volume > 0
                  ? Icons.volume_up
                  : Icons.volume_off,
              color: Colors.white,
            ),
            onPressed: () {
              setState(() {
                _controller.setVolume(
                  _controller.value.volume > 0 ? 0 : 1,
                );
              });
            },
          ),
        ],
      ),
    );
  }
}

Custom Video Player

Create a custom video player with additional features.

class CustomVideoPlayer extends StatefulWidget {
  final String videoUrl;
  final bool autoPlay;
  final bool looping;

  CustomVideoPlayer({
    required this.videoUrl,
    this.autoPlay = false,
    this.looping = false,
  });

  @override
  _CustomVideoPlayerState createState() => _CustomVideoPlayerState();
}

class _CustomVideoPlayerState extends State<CustomVideoPlayer> {
  late VideoPlayerController _controller;
  bool _isBuffering = false;

  @override
  void initState() {
    super.initState();
    _initializeVideo();
  }

  Future<void> _initializeVideo() async {
    _controller = VideoPlayerController.network(widget.videoUrl);
    await _controller.initialize();
    
    _controller.addListener(() {
      final bool isBuffering = _controller.value.isBuffering;
      if (_isBuffering != isBuffering) {
        setState(() => _isBuffering = isBuffering);
      }
    });

    if (widget.autoPlay) {
      _controller.play();
    }
    
    if (widget.looping) {
      _controller.setLooping(true);
    }
    
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    if (!_controller.value.isInitialized) {
      return Center(child: CircularProgressIndicator());
    }

    return GestureDetector(
      onTap: () {
        setState(() {
          _controller.value.isPlaying
              ? _controller.pause()
              : _controller.play();
        });
      },
      child: Stack(
        alignment: Alignment.center,
        children: [
          AspectRatio(
            aspectRatio: _controller.value.aspectRatio,
            child: VideoPlayer(_controller),
          ),
          if (_isBuffering)
            Container(
              decoration: BoxDecoration(
                color: Colors.black26,
              ),
              child: Center(
                child: CircularProgressIndicator(),
              ),
            ),
          if (!_controller.value.isPlaying)
            Container(
              decoration: BoxDecoration(
                color: Colors.black26,
              ),
              child: Icon(
                Icons.play_arrow,
                color: Colors.white,
                size: 48,
              ),
            ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

3. Audio Widgets

Audio Player Implementation

Create an audio player with controls.

class AudioPlayerExample extends StatefulWidget {
  @override
  _AudioPlayerExampleState createState() => _AudioPlayerExampleState();
}

class _AudioPlayerExampleState extends State<AudioPlayerExample> {
  final AudioPlayer _audioPlayer = AudioPlayer();
  bool _isPlaying = false;
  Duration _duration = Duration();
  Duration _position = Duration();

  @override
  void initState() {
    super.initState();
    _initAudioPlayer();
  }

  Future<void> _initAudioPlayer() async {
    await _audioPlayer.setUrl('https://example.com/audio.mp3');
    
    _audioPlayer.onDurationChanged.listen((Duration duration) {
      setState(() => _duration = duration);
    });

    _audioPlayer.onPositionChanged.listen((Duration position) {
      setState(() => _position = position);
    });

    _audioPlayer.onPlayerComplete.listen((_) {
      setState(() => _isPlaying = false);
    });
  }

  String _formatDuration(Duration duration) {
    String twoDigits(int n) => n.toString().padLeft(2, '0');
    final hours = twoDigits(duration.inHours);
    final minutes = twoDigits(duration.inMinutes.remainder(60));
    final seconds = twoDigits(duration.inSeconds.remainder(60));
    return duration.inHours > 0
        ? '$hours:$minutes:$seconds'
        : '$minutes:$seconds';
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.1),
            blurRadius: 8,
            offset: Offset(0, 4),
          ),
        ],
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            'Audio Player',
            style: TextStyle(
              fontSize: 20,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 16),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text(_formatDuration(_position)),
              Expanded(
                child: Slider(
                  value: _position.inSeconds.toDouble(),
                  min: 0.0,
                  max: _duration.inSeconds.toDouble(),
                  onChanged: (value) {
                    final position = Duration(seconds: value.toInt());
                    _audioPlayer.seek(position);
                    setState(() => _position = position);
                  },
                ),
              ),
              Text(_formatDuration(_duration)),
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              IconButton(
                icon: Icon(Icons.skip_previous),
                onPressed: () {
                  _audioPlayer.seek(Duration.zero);
                },
              ),
              IconButton(
                icon: Icon(
                  _isPlaying ? Icons.pause : Icons.play_arrow,
                  size: 32,
                ),
                onPressed: () {
                  if (_isPlaying) {
                    _audioPlayer.pause();
                  } else {
                    _audioPlayer.resume();
                  }
                  setState(() => _isPlaying = !_isPlaying);
                },
              ),
              IconButton(
                icon: Icon(Icons.stop),
                onPressed: () {
                  _audioPlayer.stop();
                  setState(() {
                    _isPlaying = false;
                    _position = Duration.zero;
                  });
                },
              ),
            ],
          ),
        ],
      ),
    );
  }

  @override
  void dispose() {
    _audioPlayer.dispose();
    super.dispose();
  }
}

4. Best Practices

1. Media Asset Management

Organize and manage media assets effectively.

// pubspec.yaml
flutter:
  assets:
    - assets/images/
    - assets/videos/
    - assets/audio/

2. Error Handling and Loading States

class MediaErrorHandler {
  static Widget handleLoadingState({
    required bool isLoading,
    required Widget child,
    Widget? loadingWidget,
  }) {
    if (isLoading) {
      return loadingWidget ??
          Center(
            child: CircularProgressIndicator(),
          );
    }
    return child;
  }

  static Widget handleError({
    required String errorMessage,
    VoidCallback? onRetry,
  }) {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(
            Icons.error_outline,
            color: Colors.red,
            size: 48,
          ),
          SizedBox(height: 16),
          Text(
            errorMessage,
            style: TextStyle(color: Colors.red),
          ),
          if (onRetry != null) ...[
            SizedBox(height: 16),
            ElevatedButton(
              onPressed: onRetry,
              child: Text('Retry'),
            ),
          ],
        ],
      ),
    );
  }
}

3. Performance Optimization

class MediaOptimization {
  static const int maxCacheSize = 100 * 1024 * 1024; // 100MB
  static const int maxCacheFileSize = 10 * 1024 * 1024; // 10MB

  static void initializeMediaCache() {
    PaintingBinding.instance.imageCache.maximumSize = 1000;
    PaintingBinding.instance.imageCache.maximumSizeBytes = maxCacheSize;
  }

  static String getOptimizedImageUrl(String url, {
    int? width,
    int? height,
    int quality = 85,
  }) {
    final Uri uri = Uri.parse(url);
    final Map<String, String> queryParams = {
      'q': quality.toString(),
      if (width != null) 'w': width.toString(),
      if (height != null) 'h': height.toString(),
    };
    return uri.replace(queryParameters: queryParams).toString();
  }
}

By following these guidelines and implementing the provided widgets correctly, you can create rich media experiences in your Flutter applications while maintaining good performance and user experience.