Skip to content

Instantly share code, notes, and snippets.

@tech-andgar
Created May 23, 2025 04:00
Show Gist options
  • Save tech-andgar/c24a0091dd8bf7f588315c7db75211a4 to your computer and use it in GitHub Desktop.
Save tech-andgar/c24a0091dd8bf7f588315c7db75211a4 to your computer and use it in GitHub Desktop.
Fix ResponsiveImage Flutter Options
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<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
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),
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<int> _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<DebouncedResponsiveImage> createState() => _DebouncedResponsiveImageState();
}
class _DebouncedResponsiveImageState extends State<DebouncedResponsiveImage> {
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<ProgressiveResponsiveImage> createState() => _ProgressiveResponsiveImageState();
}
class _ProgressiveResponsiveImageState extends State<ProgressiveResponsiveImage> {
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<int> _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<int> _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)),
],
),
),
);
},
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment