import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'dart:math' as math; void main() => runApp(MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Responsive Image Solutions', debugShowCheckedModeBanner: false, theme: ThemeData(colorSchemeSeed: Colors.blue), navigatorKey: navigatorKey, // Add the navigator key here home: const MyHomePage(title: 'Responsive Image Solutions'), ); } } // Global navigator key for context access final GlobalKey navigatorKey = GlobalKey(); class MyHomePage extends StatefulWidget { final String title; const MyHomePage({super.key, required this.title}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), backgroundColor: Theme.of(context).colorScheme.inversePrimary, ), body: SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Solution 1: Tiered Cache Sizes with cacheWidth/cacheHeight', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( 'Uses predefined cache sizes to prevent flickering while maintaining optimization.', ), const SizedBox(height: 10), SizedBox( height: 200, child: TieredResponsiveImage( imagePath: 'https://picsum.photos/id/237/2000/1500', source: ImageSourceType.network, fit: BoxFit.cover, ), ), const SizedBox(height: 30), const Text( 'Solution 2: Debounced Responsive Image', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( 'Delays cache recalculation until window resizing stops.', ), const SizedBox(height: 10), SizedBox( height: 200, child: DebouncedResponsiveImage( imagePath: 'https://picsum.photos/id/238/2000/1500', source: ImageSourceType.network, fit: BoxFit.cover, ), ), const SizedBox(height: 30), const Text( 'Solution 3: Smart Caching (Desktop vs Mobile)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( 'Different strategies for desktop and mobile platforms.', ), const SizedBox(height: 10), SizedBox( height: 200, child: SmartResponsiveImage( imagePath: 'https://picsum.photos/id/239/2000/1500', source: ImageSourceType.network, fit: BoxFit.cover, ), ), const SizedBox(height: 30), const Text( 'Solution 4: Progressive Loading with ResizeImage', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( 'Uses ResizeImage for proper low-res/high-res transition.', ), const SizedBox(height: 10), SizedBox( height: 200, child: ProgressiveResponsiveImage( imagePath: 'https://picsum.photos/id/240/2000/1500', source: ImageSourceType.network, fit: BoxFit.cover, // lowResUrl: 'optional-low-res-url-here', // If you have a separate low-res URL ), ), const SizedBox(height: 30), const Text( 'Solution 5: ResizeImage with Tiered Caching (Recommended)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), const Text( 'Combines ResizeImage benefits with tier-based caching to prevent flickering.', ), const SizedBox(height: 10), SizedBox( height: 200, child: ResizeImageWithTiers( imagePath: 'https://picsum.photos/id/241/2000/1500', source: ImageSourceType.network, fit: BoxFit.cover, ), ), ], ), ), ); } } enum ImageSourceType { network, asset } // SOLUTION 1: Tiered Cache Sizes (Most Recommended) class TieredResponsiveImage extends StatelessWidget { const TieredResponsiveImage({ required this.imagePath, required this.source, super.key, this.width, this.height, this.fit, }); final BoxFit? fit; final double? height; final String imagePath; final ImageSourceType source; final double? width; // Define cache tiers to prevent constant recalculation static const List _cacheTiers = [200, 400, 600, 800, 1200, 1600, 2400]; int _getNearestCacheTier(double size) { final targetSize = (size * 2).round(); // Account for high DPI displays for (final tier in _cacheTiers) { if (targetSize <= tier) return tier; } return _cacheTiers.last; } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final displayWidth = width ?? constraints.maxWidth; final displayHeight = height ?? constraints.maxHeight; if (!displayWidth.isFinite || !displayHeight.isFinite) { return const Center(child: Icon(Icons.broken_image_outlined)); } // Use tiered cache sizes instead of exact pixel calculations final cacheWidth = _getNearestCacheTier(displayWidth); final cacheHeight = _getNearestCacheTier(displayHeight); return _buildImage(displayWidth, displayHeight, cacheWidth, cacheHeight); }, ); } Widget _buildImage(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: _loadingBuilder, errorBuilder: _errorBuilder, ); } else { return Image.asset( imagePath, width: displayWidth, height: displayHeight, cacheWidth: cacheWidth, cacheHeight: cacheHeight, fit: fit ?? BoxFit.cover, gaplessPlayback: true, errorBuilder: _errorBuilder, ); } } Widget _loadingBuilder(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); } Widget _errorBuilder(BuildContext context, Object error, StackTrace? stackTrace) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, color: Colors.red, size: 40), SizedBox(height: 8), Text('Failed to load image', style: TextStyle(color: Colors.grey)), ], ), ); } } // SOLUTION 2: Debounced Responsive Image class DebouncedResponsiveImage extends StatefulWidget { const DebouncedResponsiveImage({ required this.imagePath, required this.source, super.key, this.width, this.height, this.fit, this.debounceMs = 300, }); final BoxFit? fit; final double? height; final String imagePath; final ImageSourceType source; final double? width; final int debounceMs; @override State createState() => _DebouncedResponsiveImageState(); } class _DebouncedResponsiveImageState extends State { int? _stableCacheWidth; int? _stableCacheHeight; DateTime _lastResizeTime = DateTime.now(); void _updateCacheSize(int newWidth, int newHeight) { _lastResizeTime = DateTime.now(); Future.delayed(Duration(milliseconds: widget.debounceMs), () { if (DateTime.now().difference(_lastResizeTime).inMilliseconds >= widget.debounceMs) { if (mounted && (_stableCacheWidth != newWidth || _stableCacheHeight != newHeight)) { setState(() { _stableCacheWidth = newWidth; _stableCacheHeight = newHeight; }); } } }); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final displayWidth = widget.width ?? constraints.maxWidth; final displayHeight = widget.height ?? constraints.maxHeight; if (!displayWidth.isFinite || !displayHeight.isFinite) { return const Center(child: Icon(Icons.broken_image_outlined)); } final pixelRatio = MediaQuery.of(context).devicePixelRatio; final newCacheWidth = (displayWidth * pixelRatio).round(); final newCacheHeight = (displayHeight * pixelRatio).round(); // Initialize cache size on first build _stableCacheWidth ??= newCacheWidth; _stableCacheHeight ??= newCacheHeight; // Update cache size with debouncing _updateCacheSize(newCacheWidth, newCacheHeight); return _buildImage(displayWidth, displayHeight, _stableCacheWidth!, _stableCacheHeight!); }, ); } Widget _buildImage(double displayWidth, double displayHeight, int cacheWidth, int cacheHeight) { if (widget.source == ImageSourceType.network) { return Image.network( widget.imagePath, width: displayWidth, height: displayHeight, cacheWidth: cacheWidth, cacheHeight: cacheHeight, fit: widget.fit ?? BoxFit.cover, gaplessPlayback: true, 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) => const Center( child: Icon(Icons.error_outline, color: Colors.red), ), ); } else { return Image.asset( widget.imagePath, width: displayWidth, height: displayHeight, cacheWidth: cacheWidth, cacheHeight: cacheHeight, fit: widget.fit ?? BoxFit.cover, gaplessPlayback: true, errorBuilder: (context, error, stackTrace) => const Center( child: Icon(Icons.broken_image_outlined, color: Colors.grey), ), ); } } } // SOLUTION 3: Smart Caching (Different strategies for desktop vs mobile) class SmartResponsiveImage extends StatelessWidget { const SmartResponsiveImage({ required this.imagePath, required this.source, super.key, this.width, this.height, this.fit, }); final BoxFit? fit; final double? height; final String imagePath; final ImageSourceType source; final double? width; bool _isDesktop(BuildContext context) { return MediaQuery.of(context).size.shortestSide > 600; } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final displayWidth = width ?? constraints.maxWidth; final displayHeight = height ?? constraints.maxHeight; if (!displayWidth.isFinite || !displayHeight.isFinite) { return const Center(child: Icon(Icons.broken_image_outlined)); } int? cacheWidth; int? cacheHeight; if (_isDesktop(context)) { // On desktop, use a higher base resolution but cap it to prevent excessive memory usage final maxCacheSize = 1200; cacheWidth = math.min((displayWidth * 1.5).round(), maxCacheSize); cacheHeight = math.min((displayHeight * 1.5).round(), maxCacheSize); } else { // On mobile, use precise pixel ratio calculation for memory efficiency final pixelRatio = MediaQuery.of(context).devicePixelRatio; cacheWidth = (displayWidth * pixelRatio).round(); cacheHeight = (displayHeight * pixelRatio).round(); } return _buildImage(displayWidth, displayHeight, cacheWidth, cacheHeight); }, ); } Widget _buildImage(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; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, ), ); }, errorBuilder: (context, error, stackTrace) => const Center( child: Icon(Icons.error_outline, color: Colors.red), ), ); } else { return Image.asset( imagePath, width: displayWidth, height: displayHeight, cacheWidth: cacheWidth, cacheHeight: cacheHeight, fit: fit ?? BoxFit.cover, gaplessPlayback: true, errorBuilder: (context, error, stackTrace) => const Center( child: Icon(Icons.broken_image_outlined, color: Colors.grey), ), ); } } } // SOLUTION 4: Progressive Loading with ResizeImage class ProgressiveResponsiveImage extends StatefulWidget { const ProgressiveResponsiveImage({ required this.imagePath, required this.source, super.key, this.width, this.height, this.fit, this.lowResUrl, // Optional separate low-res URL }); final BoxFit? fit; final double? height; final String imagePath; final ImageSourceType source; final double? width; final String? lowResUrl; // Proper way to handle low-res images @override State createState() => _ProgressiveResponsiveImageState(); } class _ProgressiveResponsiveImageState extends State { bool _showHighRes = false; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final displayWidth = widget.width ?? constraints.maxWidth; final displayHeight = widget.height ?? constraints.maxHeight; if (!displayWidth.isFinite || !displayHeight.isFinite) { return const Center(child: Icon(Icons.broken_image_outlined)); } final pixelRatio = MediaQuery.of(context).devicePixelRatio; return Stack( fit: StackFit.expand, children: [ // Low resolution image using ResizeImage (loads first and fast) _buildLowResImage(displayWidth, displayHeight, pixelRatio), // High resolution image (loads after) AnimatedOpacity( opacity: _showHighRes ? 1.0 : 0.0, duration: const Duration(milliseconds: 300), child: _buildHighResImage(displayWidth, displayHeight, pixelRatio), ), ], ); }, ); } Widget _buildLowResImage(double displayWidth, double displayHeight, double pixelRatio) { // Use very small cache size for quick initial load final lowResCacheWidth = math.min(200, (displayWidth * 0.5).round()); final lowResCacheHeight = math.min(200, (displayHeight * 0.5).round()); ImageProvider imageProvider; if (widget.source == ImageSourceType.network) { // Use provided low-res URL if available, otherwise same URL with small cache final imageUrl = widget.lowResUrl ?? widget.imagePath; imageProvider = ResizeImage( NetworkImage(imageUrl), width: lowResCacheWidth, height: lowResCacheHeight, ); } else { imageProvider = ResizeImage( AssetImage(widget.imagePath), width: lowResCacheWidth, height: lowResCacheHeight, ); } return Image( image: imageProvider, width: displayWidth, height: displayHeight, fit: widget.fit ?? BoxFit.cover, gaplessPlayback: true, ); } Widget _buildHighResImage(double displayWidth, double displayHeight, double pixelRatio) { // Use tiered cache size for high-res image final cacheWidth = _getNearestCacheTier(displayWidth * pixelRatio); final cacheHeight = _getNearestCacheTier(displayHeight * pixelRatio); ImageProvider imageProvider; if (widget.source == ImageSourceType.network) { imageProvider = ResizeImage( NetworkImage(widget.imagePath), width: cacheWidth, height: cacheHeight, ); } else { imageProvider = ResizeImage( AssetImage(widget.imagePath), width: cacheWidth, height: cacheHeight, ); } return Image( image: imageProvider, width: displayWidth, height: displayHeight, fit: widget.fit ?? BoxFit.cover, gaplessPlayback: true, loadingBuilder: (context, child, loadingProgress) { if (loadingProgress == null) { // Image loaded, show it WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) setState(() => _showHighRes = true); }); return child; } return const SizedBox.shrink(); // Hide while loading }, errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(), ); } // Helper method for cache tiers static const List _cacheTiers = [200, 400, 600, 800, 1200, 1600, 2400]; int _getNearestCacheTier(double size) { final targetSize = size.round(); for (final tier in _cacheTiers) { if (targetSize <= tier) return tier; } return _cacheTiers.last; } } // SOLUTION 5: ResizeImage with Tiered Caching (Best of both worlds) class ResizeImageWithTiers extends StatelessWidget { const ResizeImageWithTiers({ required this.imagePath, required this.source, super.key, this.width, this.height, this.fit, }); final BoxFit? fit; final double? height; final String imagePath; final ImageSourceType source; final double? width; // Cache tiers to prevent flickering static const List _cacheTiers = [200, 400, 600, 800, 1200, 1600, 2400]; int _getNearestCacheTier(double size) { final targetSize = (size * MediaQuery.of(navigatorKey.currentContext!).devicePixelRatio).round(); for (final tier in _cacheTiers) { if (targetSize <= tier) return tier; } return _cacheTiers.last; } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final displayWidth = width ?? constraints.maxWidth; final displayHeight = height ?? constraints.maxHeight; if (!displayWidth.isFinite || !displayHeight.isFinite) { return const Center(child: Icon(Icons.broken_image_outlined)); } // Use tiered cache sizes to prevent flickering final cacheWidth = _getNearestCacheTier(displayWidth); final cacheHeight = _getNearestCacheTier(displayHeight); ImageProvider imageProvider; if (source == ImageSourceType.network) { imageProvider = ResizeImage( NetworkImage(imagePath), width: cacheWidth, height: cacheHeight, ); } else { imageProvider = ResizeImage( AssetImage(imagePath), width: cacheWidth, height: cacheHeight, ); } return Image( image: imageProvider, width: displayWidth, height: displayHeight, fit: fit ?? BoxFit.cover, gaplessPlayback: true, 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) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, color: Colors.red, size: 40), SizedBox(height: 8), Text('Failed to load image', style: TextStyle(color: Colors.grey)), ], ), ), ); }, ); } }