Created
May 23, 2025 04:14
-
-
Save tech-andgar/84c969a0831e13405a504b0ca5a27ce3 to your computer and use it in GitHub Desktop.
Fix OptimizedResponsiveImage Flutter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/material.dart'; | |
| import 'package:flutter/foundation.dart' show kIsWeb; | |
| import 'dart:math' as math; | |
| void main() => runApp(const MyApp()); | |
| class MyApp extends StatelessWidget { | |
| const MyApp({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return MaterialApp( | |
| title: 'Flutter Demo', | |
| debugShowCheckedModeBanner: false, | |
| theme: ThemeData( | |
| colorSchemeSeed: Colors.blue, | |
| ), | |
| home: const MyHomePage(title: 'Flutter Demo Home Page'), | |
| ); | |
| } | |
| } | |
| class MyHomePage extends StatefulWidget { | |
| final String title; | |
| const MyHomePage({ | |
| super.key, | |
| required this.title, | |
| }); | |
| @override | |
| State<MyHomePage> createState() => _MyHomePageState(); | |
| } | |
| class _MyHomePageState extends State<MyHomePage> { | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar( | |
| title: Text(widget.title), | |
| ), | |
| body: ResponsiveImageExample() | |
| ); | |
| } | |
| } | |
| enum ImageSourceType { network, asset } | |
| /// An optimized responsive image widget that prevents flickering during window resizes | |
| /// while maintaining memory efficiency and image quality. | |
| class OptimizedResponsiveImage extends StatelessWidget { | |
| const OptimizedResponsiveImage({ | |
| required this.imagePath, | |
| required this.source, | |
| super.key, | |
| this.width, | |
| this.height, | |
| this.fit, | |
| this.enableProgressiveLoading = false, | |
| this.lowResImagePath, | |
| this.memoryOptimization = MemoryOptimization.balanced, | |
| }); | |
| final BoxFit? fit; | |
| final double? height; | |
| final String imagePath; | |
| final ImageSourceType source; | |
| final double? width; | |
| final bool enableProgressiveLoading; | |
| final String? lowResImagePath; // Optional low-res version for progressive loading | |
| final MemoryOptimization memoryOptimization; | |
| // Optimized cache tiers based on common screen sizes and memory constraints | |
| static const List<int> _mobileCacheTiers = [200, 400, 600, 800, 1200]; | |
| static const List<int> _desktopCacheTiers = [400, 600, 800, 1200, 1600, 2400]; | |
| static const List<int> _webCacheTiers = [300, 500, 800, 1200, 1800]; | |
| /// Determines the appropriate cache tiers based on platform and screen size | |
| List<int> _getCacheTiers(BuildContext context) { | |
| if (kIsWeb) return _webCacheTiers; | |
| final screenSize = MediaQuery.of(context).size; | |
| final isTabletOrDesktop = screenSize.shortestSide > 600; | |
| return isTabletOrDesktop ? _desktopCacheTiers : _mobileCacheTiers; | |
| } | |
| /// Gets the nearest cache tier size to minimize re-decoding | |
| int _getNearestCacheTier(double targetSize, List<int> tiers) { | |
| // Apply memory optimization strategy | |
| final adjustedTarget = switch (memoryOptimization) { | |
| MemoryOptimization.aggressive => targetSize * 0.8, | |
| MemoryOptimization.balanced => targetSize, | |
| MemoryOptimization.quality => targetSize * 1.2, | |
| }; | |
| final roundedTarget = adjustedTarget.round(); | |
| // Find the smallest tier that can accommodate the target | |
| for (final tier in tiers) { | |
| if (roundedTarget <= tier) return tier; | |
| } | |
| // If target exceeds all tiers, return the largest tier | |
| return tiers.last; | |
| } | |
| /// Calculates optimal cache dimensions considering device pixel ratio and platform | |
| ({int width, int height}) _calculateCacheDimensions( | |
| BuildContext context, | |
| double displayWidth, | |
| double displayHeight, | |
| ) { | |
| final pixelRatio = MediaQuery.of(context).devicePixelRatio; | |
| final cacheTiers = _getCacheTiers(context); | |
| // Calculate target physical dimensions | |
| double targetWidth = displayWidth * pixelRatio; | |
| double targetHeight = displayHeight * pixelRatio; | |
| // Platform-specific adjustments | |
| if (kIsWeb) { | |
| // Web often has different pixel ratios and scaling behavior | |
| targetWidth = math.min(targetWidth, 1920); // Cap for web performance | |
| targetHeight = math.min(targetHeight, 1920); | |
| } | |
| // Get tiered cache dimensions | |
| final cacheWidth = _getNearestCacheTier(targetWidth, cacheTiers); | |
| final cacheHeight = _getNearestCacheTier(targetHeight, cacheTiers); | |
| return (width: cacheWidth, height: cacheHeight); | |
| } | |
| Widget _buildErrorWidget(String message, {double? iconSize}) { | |
| return Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| Icon( | |
| source == ImageSourceType.network ? Icons.error_outline : Icons.broken_image_outlined, | |
| color: Colors.red.shade400, | |
| size: iconSize?.clamp(20.0, 60.0) ?? 40.0, | |
| ), | |
| const SizedBox(height: 8), | |
| Text( | |
| message, | |
| style: TextStyle(color: Colors.grey.shade600, fontSize: 12), | |
| textAlign: TextAlign.center, | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Widget _buildLoadingWidget() { | |
| return const Center( | |
| child: CircularProgressIndicator(strokeWidth: 2), | |
| ); | |
| } | |
| Widget _buildImage( | |
| BuildContext context, | |
| double displayWidth, | |
| double displayHeight, | |
| int cacheWidth, | |
| int cacheHeight, | |
| ) { | |
| if (source == ImageSourceType.network) { | |
| return Image.network( | |
| imagePath, | |
| width: displayWidth, | |
| height: displayHeight, | |
| cacheWidth: cacheWidth, | |
| cacheHeight: cacheHeight, | |
| fit: fit ?? BoxFit.cover, | |
| gaplessPlayback: true, | |
| loadingBuilder: (context, child, loadingProgress) { | |
| if (loadingProgress == null) return child; | |
| final progress = loadingProgress.expectedTotalBytes != null | |
| ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! | |
| : null; | |
| return Center( | |
| child: CircularProgressIndicator( | |
| value: progress, | |
| strokeWidth: 2, | |
| ), | |
| ); | |
| }, | |
| errorBuilder: (context, error, stackTrace) { | |
| debugPrint('Failed to load network image: $imagePath - $error'); | |
| return _buildErrorWidget('Failed to load image'); | |
| }, | |
| ); | |
| } else { | |
| return Image.asset( | |
| imagePath, | |
| width: displayWidth, | |
| height: displayHeight, | |
| cacheWidth: cacheWidth, | |
| cacheHeight: cacheHeight, | |
| fit: fit ?? BoxFit.cover, | |
| gaplessPlayback: true, | |
| errorBuilder: (context, error, stackTrace) { | |
| debugPrint('Failed to load asset image: $imagePath - $error'); | |
| return _buildErrorWidget('Failed to load asset'); | |
| }, | |
| ); | |
| } | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return LayoutBuilder( | |
| builder: (context, constraints) { | |
| final displayWidth = width ?? constraints.maxWidth; | |
| final displayHeight = height ?? constraints.maxHeight; | |
| // Validate dimensions | |
| if (!displayWidth.isFinite || !displayHeight.isFinite || | |
| displayWidth <= 0 || displayHeight <= 0) { | |
| return _buildErrorWidget( | |
| 'Invalid dimensions', | |
| iconSize: (width ?? height ?? 50).clamp(20.0, 100.0), | |
| ); | |
| } | |
| // Calculate optimal cache dimensions | |
| final cacheDimensions = _calculateCacheDimensions( | |
| context, | |
| displayWidth, | |
| displayHeight, | |
| ); | |
| // Build the appropriate image widget | |
| if (enableProgressiveLoading && lowResImagePath != null) { | |
| return _ProgressiveImageLoader( | |
| highResImage: _buildImage( | |
| context, | |
| displayWidth, | |
| displayHeight, | |
| cacheDimensions.width, | |
| cacheDimensions.height, | |
| ), | |
| lowResImage: OptimizedResponsiveImage( | |
| imagePath: lowResImagePath!, | |
| source: source, | |
| width: displayWidth, | |
| height: displayHeight, | |
| fit: fit, | |
| memoryOptimization: MemoryOptimization.aggressive, | |
| ), | |
| ); | |
| } | |
| return _buildImage( | |
| context, | |
| displayWidth, | |
| displayHeight, | |
| cacheDimensions.width, | |
| cacheDimensions.height, | |
| ); | |
| }, | |
| ); | |
| } | |
| } | |
| /// Memory optimization strategies for different use cases | |
| enum MemoryOptimization { | |
| /// Prioritize memory efficiency over image quality | |
| aggressive, | |
| /// Balance between memory usage and image quality | |
| balanced, | |
| /// Prioritize image quality over memory efficiency | |
| quality, | |
| } | |
| /// Progressive image loader for smooth low-to-high resolution transitions | |
| class _ProgressiveImageLoader extends StatefulWidget { | |
| const _ProgressiveImageLoader({ | |
| required this.highResImage, | |
| required this.lowResImage, | |
| }); | |
| final Widget highResImage; | |
| final Widget lowResImage; | |
| @override | |
| State<_ProgressiveImageLoader> createState() => _ProgressiveImageLoaderState(); | |
| } | |
| class _ProgressiveImageLoaderState extends State<_ProgressiveImageLoader> { | |
| bool _showHighRes = false; | |
| @override | |
| Widget build(BuildContext context) { | |
| return Stack( | |
| fit: StackFit.expand, | |
| children: [ | |
| // Low resolution image (shows immediately) | |
| widget.lowResImage, | |
| // High resolution image (fades in when loaded) | |
| AnimatedOpacity( | |
| opacity: _showHighRes ? 1.0 : 0.0, | |
| duration: const Duration(milliseconds: 300), | |
| curve: Curves.easeInOut, | |
| child: widget.highResImage, | |
| ), | |
| ], | |
| ); | |
| } | |
| @override | |
| void initState() { | |
| super.initState(); | |
| // Trigger high-res image load after a short delay | |
| WidgetsBinding.instance.addPostFrameCallback((_) { | |
| Future.delayed(const Duration(milliseconds: 100), () { | |
| if (mounted) { | |
| setState(() => _showHighRes = true); | |
| } | |
| }); | |
| }); | |
| } | |
| } | |
| /// Example usage demonstrating different optimization strategies | |
| class ResponsiveImageExample extends StatelessWidget { | |
| const ResponsiveImageExample({super.key}); | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| appBar: AppBar(title: const Text('Optimized Responsive Images')), | |
| body: SingleChildScrollView( | |
| padding: const EdgeInsets.all(16), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| _buildSection( | |
| 'Balanced Optimization (Default)', | |
| 'Best for most use cases', | |
| OptimizedResponsiveImage( | |
| imagePath: 'https://picsum.photos/id/237/2000/1500', | |
| source: ImageSourceType.network, | |
| fit: BoxFit.cover, | |
| memoryOptimization: MemoryOptimization.balanced, | |
| ), | |
| ), | |
| _buildSection( | |
| 'Aggressive Memory Optimization', | |
| 'Lower memory usage, slight quality reduction', | |
| OptimizedResponsiveImage( | |
| imagePath: 'https://picsum.photos/id/238/2000/1500', | |
| source: ImageSourceType.network, | |
| fit: BoxFit.cover, | |
| memoryOptimization: MemoryOptimization.aggressive, | |
| ), | |
| ), | |
| _buildSection( | |
| 'Quality Priority', | |
| 'Higher quality, more memory usage', | |
| OptimizedResponsiveImage( | |
| imagePath: 'https://picsum.photos/id/239/2000/1500', | |
| source: ImageSourceType.network, | |
| fit: BoxFit.cover, | |
| memoryOptimization: MemoryOptimization.quality, | |
| ), | |
| ), | |
| _buildSection( | |
| 'Progressive Loading', | |
| 'Smooth transition from low to high resolution', | |
| OptimizedResponsiveImage( | |
| imagePath: 'https://picsum.photos/id/240/2000/1500', | |
| source: ImageSourceType.network, | |
| fit: BoxFit.cover, | |
| enableProgressiveLoading: true, | |
| lowResImagePath: 'https://picsum.photos/id/240/400/300', | |
| memoryOptimization: MemoryOptimization.balanced, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| Widget _buildSection(String title, String description, Widget imageWidget) { | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), | |
| const SizedBox(height: 4), | |
| Text(description, style: TextStyle(color: Colors.grey.shade600)), | |
| const SizedBox(height: 12), | |
| SizedBox(height: 300, child: imageWidget), | |
| const SizedBox(height: 24), | |
| ], | |
| ); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment