Skip to content

Instantly share code, notes, and snippets.

@tech-andgar
Created May 23, 2025 04:14
Show Gist options
  • Save tech-andgar/84c969a0831e13405a504b0ca5a27ce3 to your computer and use it in GitHub Desktop.
Save tech-andgar/84c969a0831e13405a504b0ca5a27ce3 to your computer and use it in GitHub Desktop.
Fix OptimizedResponsiveImage Flutter
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