295 lines
7.0 KiB
Dart
295 lines
7.0 KiB
Dart
/// Performance-optimized cached network image widget
|
|
///
|
|
/// Features:
|
|
/// - Automatic memory and disk caching
|
|
/// - Optimized image sizing to reduce memory usage
|
|
/// - Smooth fade-in animations
|
|
/// - Shimmer loading placeholders
|
|
/// - Graceful error handling
|
|
/// - RepaintBoundary for isolation
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import '../config/image_cache_config.dart';
|
|
import '../constants/performance_constants.dart';
|
|
|
|
/// Optimized cached network image with performance enhancements
|
|
class OptimizedCachedImage extends StatelessWidget {
|
|
final String? imageUrl;
|
|
final ImageContext context;
|
|
final BoxFit fit;
|
|
final double? width;
|
|
final double? height;
|
|
final Widget? placeholder;
|
|
final Widget? errorWidget;
|
|
final bool useRepaintBoundary;
|
|
|
|
const OptimizedCachedImage({
|
|
super.key,
|
|
required this.imageUrl,
|
|
this.context = ImageContext.gridThumbnail,
|
|
this.fit = BoxFit.cover,
|
|
this.width,
|
|
this.height,
|
|
this.placeholder,
|
|
this.errorWidget,
|
|
this.useRepaintBoundary = true,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final image = _buildImage();
|
|
|
|
// Wrap in RepaintBoundary for better performance
|
|
return useRepaintBoundary
|
|
? RepaintBoundary(child: image)
|
|
: image;
|
|
}
|
|
|
|
Widget _buildImage() {
|
|
if (imageUrl == null || imageUrl!.isEmpty) {
|
|
return _buildErrorWidget();
|
|
}
|
|
|
|
// Get optimal dimensions for this context
|
|
final dimensions = ImageOptimization.getOptimalDimensions(
|
|
screenWidth: width ?? 300,
|
|
context: this.context,
|
|
);
|
|
|
|
// Choose appropriate cache manager
|
|
final cacheManager = this.context == ImageContext.categoryCard
|
|
? CategoryImageCacheManager()
|
|
: ProductImageCacheManager();
|
|
|
|
return CachedNetworkImage(
|
|
imageUrl: imageUrl!,
|
|
cacheManager: cacheManager,
|
|
|
|
// Performance optimization: resize in memory
|
|
memCacheWidth: dimensions.width,
|
|
memCacheHeight: dimensions.height,
|
|
|
|
// Performance optimization: resize on disk
|
|
maxWidthDiskCache: dimensions.width * 2,
|
|
maxHeightDiskCache: dimensions.height * 2,
|
|
|
|
// Sizing
|
|
width: width,
|
|
height: height,
|
|
fit: fit,
|
|
|
|
// Smooth fade-in animation
|
|
fadeInDuration: Duration(
|
|
milliseconds: PerformanceConstants.imageFadeDuration,
|
|
),
|
|
fadeOutDuration: Duration(
|
|
milliseconds: PerformanceConstants.fastAnimationDuration,
|
|
),
|
|
|
|
// Placeholder while loading
|
|
placeholder: (context, url) =>
|
|
placeholder ?? _buildPlaceholder(),
|
|
|
|
// Error widget if loading fails
|
|
errorWidget: (context, url, error) =>
|
|
errorWidget ?? _buildErrorWidget(),
|
|
);
|
|
}
|
|
|
|
Widget _buildPlaceholder() {
|
|
return Container(
|
|
width: width,
|
|
height: height,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[200],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: const Center(
|
|
child: ShimmerPlaceholder(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildErrorWidget() {
|
|
return Container(
|
|
width: width,
|
|
height: height,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Icon(
|
|
Icons.image_not_supported_outlined,
|
|
color: Colors.grey[400],
|
|
size: 48,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Shimmer loading placeholder for better UX
|
|
class ShimmerPlaceholder extends StatefulWidget {
|
|
final double? width;
|
|
final double? height;
|
|
final BorderRadius? borderRadius;
|
|
|
|
const ShimmerPlaceholder({
|
|
super.key,
|
|
this.width,
|
|
this.height,
|
|
this.borderRadius,
|
|
});
|
|
|
|
@override
|
|
State<ShimmerPlaceholder> createState() => _ShimmerPlaceholderState();
|
|
}
|
|
|
|
class _ShimmerPlaceholderState extends State<ShimmerPlaceholder>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
duration: Duration(
|
|
milliseconds: PerformanceConstants.shimmerDuration,
|
|
),
|
|
vsync: this,
|
|
)..repeat();
|
|
|
|
_animation = Tween<double>(begin: -2, end: 2).animate(
|
|
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return Container(
|
|
width: widget.width,
|
|
height: widget.height,
|
|
decoration: BoxDecoration(
|
|
borderRadius: widget.borderRadius ?? BorderRadius.circular(8),
|
|
gradient: LinearGradient(
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
colors: [
|
|
Colors.grey[200]!,
|
|
Colors.grey[100]!,
|
|
Colors.grey[200]!,
|
|
],
|
|
stops: const [0.0, 0.5, 1.0],
|
|
transform: GradientRotation(_animation.value),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Product grid image - optimized for grid display
|
|
class ProductGridImage extends StatelessWidget {
|
|
final String? imageUrl;
|
|
final double size;
|
|
|
|
const ProductGridImage({
|
|
super.key,
|
|
required this.imageUrl,
|
|
this.size = 150,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return OptimizedCachedImage(
|
|
imageUrl: imageUrl,
|
|
context: ImageContext.gridThumbnail,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.cover,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Category card image - optimized for category display
|
|
class CategoryCardImage extends StatelessWidget {
|
|
final String? imageUrl;
|
|
final double size;
|
|
|
|
const CategoryCardImage({
|
|
super.key,
|
|
required this.imageUrl,
|
|
this.size = 120,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return OptimizedCachedImage(
|
|
imageUrl: imageUrl,
|
|
context: ImageContext.categoryCard,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.cover,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Cart item thumbnail - very small optimized image
|
|
class CartItemThumbnail extends StatelessWidget {
|
|
final String? imageUrl;
|
|
final double size;
|
|
|
|
const CartItemThumbnail({
|
|
super.key,
|
|
required this.imageUrl,
|
|
this.size = 60,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return OptimizedCachedImage(
|
|
imageUrl: imageUrl,
|
|
context: ImageContext.cartThumbnail,
|
|
width: size,
|
|
height: size,
|
|
fit: BoxFit.cover,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Product detail image - larger but still optimized
|
|
class ProductDetailImage extends StatelessWidget {
|
|
final String? imageUrl;
|
|
final double? width;
|
|
final double? height;
|
|
|
|
const ProductDetailImage({
|
|
super.key,
|
|
required this.imageUrl,
|
|
this.width,
|
|
this.height,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return OptimizedCachedImage(
|
|
imageUrl: imageUrl,
|
|
context: ImageContext.detail,
|
|
width: width,
|
|
height: height,
|
|
fit: BoxFit.contain,
|
|
);
|
|
}
|
|
}
|