runable
This commit is contained in:
75
lib/core/widgets/custom_button.dart
Normal file
75
lib/core/widgets/custom_button.dart
Normal file
@@ -0,0 +1,75 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../constants/ui_constants.dart';
|
||||
|
||||
/// Custom button widget
|
||||
class CustomButton extends StatelessWidget {
|
||||
final String text;
|
||||
final VoidCallback? onPressed;
|
||||
final bool isLoading;
|
||||
final bool isOutlined;
|
||||
final IconData? icon;
|
||||
final Color? backgroundColor;
|
||||
|
||||
const CustomButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.onPressed,
|
||||
this.isLoading = false,
|
||||
this.isOutlined = false,
|
||||
this.icon,
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isOutlined) {
|
||||
return OutlinedButton.icon(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
width: UIConstants.iconSizeS,
|
||||
height: UIConstants.iconSizeS,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Icon(icon ?? Icons.check),
|
||||
label: Text(text),
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, UIConstants.buttonHeightM),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
return ElevatedButton.icon(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
icon: isLoading
|
||||
? const SizedBox(
|
||||
width: UIConstants.iconSizeS,
|
||||
height: UIConstants.iconSizeS,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Icon(icon),
|
||||
label: Text(text),
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, UIConstants.buttonHeightM),
|
||||
backgroundColor: backgroundColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: isLoading ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, UIConstants.buttonHeightM),
|
||||
backgroundColor: backgroundColor,
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: UIConstants.iconSizeM,
|
||||
height: UIConstants.iconSizeM,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(text),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/core/widgets/empty_state.dart
Normal file
61
lib/core/widgets/empty_state.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Empty state widget
|
||||
class EmptyState extends StatelessWidget {
|
||||
final String message;
|
||||
final String? subMessage;
|
||||
final IconData? icon;
|
||||
final VoidCallback? onAction;
|
||||
final String? actionText;
|
||||
|
||||
const EmptyState({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.subMessage,
|
||||
this.icon,
|
||||
this.onAction,
|
||||
this.actionText,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.inbox_outlined,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (subMessage != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subMessage!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (onAction != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: onAction,
|
||||
child: Text(actionText ?? 'Take Action'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/core/widgets/error_widget.dart
Normal file
48
lib/core/widgets/error_widget.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Error display widget
|
||||
class ErrorDisplay extends StatelessWidget {
|
||||
final String message;
|
||||
final VoidCallback? onRetry;
|
||||
final IconData? icon;
|
||||
|
||||
const ErrorDisplay({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.onRetry,
|
||||
this.icon,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.error_outline,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
36
lib/core/widgets/loading_indicator.dart
Normal file
36
lib/core/widgets/loading_indicator.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Loading indicator widget
|
||||
class LoadingIndicator extends StatelessWidget {
|
||||
final String? message;
|
||||
final double? size;
|
||||
|
||||
const LoadingIndicator({
|
||||
super.key,
|
||||
this.message,
|
||||
this.size,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size ?? 50,
|
||||
height: size ?? 50,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message!,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
294
lib/core/widgets/optimized_cached_image.dart
Normal file
294
lib/core/widgets/optimized_cached_image.dart
Normal file
@@ -0,0 +1,294 @@
|
||||
/// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
339
lib/core/widgets/optimized_grid_view.dart
Normal file
339
lib/core/widgets/optimized_grid_view.dart
Normal file
@@ -0,0 +1,339 @@
|
||||
/// Performance-optimized GridView implementation
|
||||
///
|
||||
/// Features:
|
||||
/// - Automatic RepaintBoundary for grid items
|
||||
/// - Optimized scrolling physics
|
||||
/// - Responsive column count
|
||||
/// - Efficient caching and preloading
|
||||
/// - Proper key management for widget identity
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../constants/performance_constants.dart';
|
||||
|
||||
/// Optimized GridView.builder with performance enhancements
|
||||
class OptimizedGridView<T> extends StatelessWidget {
|
||||
final List<T> items;
|
||||
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
||||
final ScrollController? scrollController;
|
||||
final EdgeInsets? padding;
|
||||
final bool shrinkWrap;
|
||||
final ScrollPhysics? physics;
|
||||
final double? crossAxisSpacing;
|
||||
final double? mainAxisSpacing;
|
||||
final double? childAspectRatio;
|
||||
final int? crossAxisCount;
|
||||
final bool useRepaintBoundary;
|
||||
|
||||
const OptimizedGridView({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
this.scrollController,
|
||||
this.padding,
|
||||
this.shrinkWrap = false,
|
||||
this.physics,
|
||||
this.crossAxisSpacing,
|
||||
this.mainAxisSpacing,
|
||||
this.childAspectRatio,
|
||||
this.crossAxisCount,
|
||||
this.useRepaintBoundary = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
return GridView.builder(
|
||||
controller: scrollController,
|
||||
padding: padding ?? const EdgeInsets.all(12),
|
||||
shrinkWrap: shrinkWrap,
|
||||
|
||||
// Optimized physics for smooth scrolling
|
||||
physics: physics ?? const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
|
||||
// Performance optimization: preload items
|
||||
cacheExtent: PerformanceConstants.getCacheExtent(screenHeight),
|
||||
|
||||
itemCount: items.length,
|
||||
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount ??
|
||||
PerformanceConstants.getGridColumnCount(screenWidth),
|
||||
crossAxisSpacing: crossAxisSpacing ?? PerformanceConstants.gridSpacing,
|
||||
mainAxisSpacing: mainAxisSpacing ?? PerformanceConstants.gridSpacing,
|
||||
childAspectRatio: childAspectRatio ??
|
||||
PerformanceConstants.productCardAspectRatio,
|
||||
),
|
||||
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final child = itemBuilder(context, item, index);
|
||||
|
||||
// Wrap in RepaintBoundary for better performance
|
||||
return useRepaintBoundary
|
||||
? RepaintBoundary(
|
||||
// Use ValueKey for stable widget identity
|
||||
key: ValueKey('grid_item_$index'),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimized GridView for products
|
||||
class ProductGridView<T> extends StatelessWidget {
|
||||
final List<T> products;
|
||||
final Widget Function(BuildContext context, T product, int index) itemBuilder;
|
||||
final ScrollController? scrollController;
|
||||
final VoidCallback? onScrollEnd;
|
||||
|
||||
const ProductGridView({
|
||||
super.key,
|
||||
required this.products,
|
||||
required this.itemBuilder,
|
||||
this.scrollController,
|
||||
this.onScrollEnd,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = scrollController ?? ScrollController();
|
||||
|
||||
// Add scroll listener for infinite scroll
|
||||
if (onScrollEnd != null) {
|
||||
controller.addListener(() {
|
||||
if (controller.position.pixels >=
|
||||
controller.position.maxScrollExtent - 200) {
|
||||
onScrollEnd!();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return OptimizedGridView<T>(
|
||||
items: products,
|
||||
itemBuilder: itemBuilder,
|
||||
scrollController: controller,
|
||||
childAspectRatio: PerformanceConstants.productCardAspectRatio,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimized GridView for categories
|
||||
class CategoryGridView<T> extends StatelessWidget {
|
||||
final List<T> categories;
|
||||
final Widget Function(BuildContext context, T category, int index) itemBuilder;
|
||||
final ScrollController? scrollController;
|
||||
|
||||
const CategoryGridView({
|
||||
super.key,
|
||||
required this.categories,
|
||||
required this.itemBuilder,
|
||||
this.scrollController,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OptimizedGridView<T>(
|
||||
items: categories,
|
||||
itemBuilder: itemBuilder,
|
||||
scrollController: scrollController,
|
||||
childAspectRatio: PerformanceConstants.categoryCardAspectRatio,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimized sliver grid for use in CustomScrollView
|
||||
class OptimizedSliverGrid<T> extends StatelessWidget {
|
||||
final List<T> items;
|
||||
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
||||
final double? crossAxisSpacing;
|
||||
final double? mainAxisSpacing;
|
||||
final double? childAspectRatio;
|
||||
final int? crossAxisCount;
|
||||
final bool useRepaintBoundary;
|
||||
|
||||
const OptimizedSliverGrid({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
this.crossAxisSpacing,
|
||||
this.mainAxisSpacing,
|
||||
this.childAspectRatio,
|
||||
this.crossAxisCount,
|
||||
this.useRepaintBoundary = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
return SliverGrid(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final item = items[index];
|
||||
final child = itemBuilder(context, item, index);
|
||||
|
||||
return useRepaintBoundary
|
||||
? RepaintBoundary(
|
||||
key: ValueKey('sliver_grid_item_$index'),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
},
|
||||
childCount: items.length,
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount ??
|
||||
PerformanceConstants.getGridColumnCount(screenWidth),
|
||||
crossAxisSpacing: crossAxisSpacing ?? PerformanceConstants.gridSpacing,
|
||||
mainAxisSpacing: mainAxisSpacing ?? PerformanceConstants.gridSpacing,
|
||||
childAspectRatio: childAspectRatio ??
|
||||
PerformanceConstants.productCardAspectRatio,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty state widget for grids
|
||||
class GridEmptyState extends StatelessWidget {
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
const GridEmptyState({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.icon = Icons.inventory_2_outlined,
|
||||
this.onRetry,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 80,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Loading state for grid
|
||||
class GridLoadingState extends StatelessWidget {
|
||||
final int itemCount;
|
||||
|
||||
const GridLoadingState({
|
||||
super.key,
|
||||
this.itemCount = 6,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final crossAxisCount = PerformanceConstants.getGridColumnCount(screenWidth);
|
||||
|
||||
return GridView.builder(
|
||||
padding: const EdgeInsets.all(12),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: PerformanceConstants.gridSpacing,
|
||||
mainAxisSpacing: PerformanceConstants.gridSpacing,
|
||||
childAspectRatio: PerformanceConstants.productCardAspectRatio,
|
||||
),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
return const GridShimmerItem();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shimmer item for grid loading state
|
||||
class GridShimmerItem extends StatelessWidget {
|
||||
const GridShimmerItem({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 14,
|
||||
width: 80,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
258
lib/core/widgets/optimized_list_view.dart
Normal file
258
lib/core/widgets/optimized_list_view.dart
Normal file
@@ -0,0 +1,258 @@
|
||||
/// Performance-optimized ListView implementation
|
||||
///
|
||||
/// Features:
|
||||
/// - Automatic RepaintBoundary for list items
|
||||
/// - Optimized scrolling with physics
|
||||
/// - Efficient caching and preloading
|
||||
/// - Fixed itemExtent for better performance
|
||||
/// - Proper key management
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import '../constants/performance_constants.dart';
|
||||
|
||||
/// Optimized ListView.builder with performance enhancements
|
||||
class OptimizedListView<T> extends StatelessWidget {
|
||||
final List<T> items;
|
||||
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
||||
final ScrollController? scrollController;
|
||||
final EdgeInsets? padding;
|
||||
final bool shrinkWrap;
|
||||
final ScrollPhysics? physics;
|
||||
final double? itemExtent;
|
||||
final Widget? separator;
|
||||
final bool useRepaintBoundary;
|
||||
|
||||
const OptimizedListView({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
this.scrollController,
|
||||
this.padding,
|
||||
this.shrinkWrap = false,
|
||||
this.physics,
|
||||
this.itemExtent,
|
||||
this.separator,
|
||||
this.useRepaintBoundary = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
|
||||
if (separator != null) {
|
||||
return ListView.separated(
|
||||
controller: scrollController,
|
||||
padding: padding ?? const EdgeInsets.all(12),
|
||||
shrinkWrap: shrinkWrap,
|
||||
physics: physics ?? const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
cacheExtent: PerformanceConstants.getCacheExtent(screenHeight),
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) => separator!,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final child = itemBuilder(context, item, index);
|
||||
|
||||
return useRepaintBoundary
|
||||
? RepaintBoundary(
|
||||
key: ValueKey('list_item_$index'),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
controller: scrollController,
|
||||
padding: padding ?? const EdgeInsets.all(12),
|
||||
shrinkWrap: shrinkWrap,
|
||||
physics: physics ?? const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
cacheExtent: PerformanceConstants.getCacheExtent(screenHeight),
|
||||
itemExtent: itemExtent,
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final child = itemBuilder(context, item, index);
|
||||
|
||||
return useRepaintBoundary
|
||||
? RepaintBoundary(
|
||||
key: ValueKey('list_item_$index'),
|
||||
child: child,
|
||||
)
|
||||
: child;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Optimized ListView for cart items
|
||||
class CartListView<T> extends StatelessWidget {
|
||||
final List<T> items;
|
||||
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
||||
final ScrollController? scrollController;
|
||||
final VoidCallback? onScrollEnd;
|
||||
|
||||
const CartListView({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
this.scrollController,
|
||||
this.onScrollEnd,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = scrollController ?? ScrollController();
|
||||
|
||||
if (onScrollEnd != null) {
|
||||
controller.addListener(() {
|
||||
if (controller.position.pixels >=
|
||||
controller.position.maxScrollExtent - 100) {
|
||||
onScrollEnd!();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return OptimizedListView<T>(
|
||||
items: items,
|
||||
itemBuilder: itemBuilder,
|
||||
scrollController: controller,
|
||||
separator: const Divider(height: 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty state widget for lists
|
||||
class ListEmptyState extends StatelessWidget {
|
||||
final String message;
|
||||
final IconData icon;
|
||||
final VoidCallback? onAction;
|
||||
final String? actionLabel;
|
||||
|
||||
const ListEmptyState({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.icon = Icons.inbox_outlined,
|
||||
this.onAction,
|
||||
this.actionLabel,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 80,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onAction != null && actionLabel != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: onAction,
|
||||
child: Text(actionLabel!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Loading state for list
|
||||
class ListLoadingState extends StatelessWidget {
|
||||
final int itemCount;
|
||||
final double itemHeight;
|
||||
|
||||
const ListLoadingState({
|
||||
super.key,
|
||||
this.itemCount = 10,
|
||||
this.itemHeight = 80,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(12),
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: itemCount,
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
return ListShimmerItem(height: itemHeight);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Shimmer item for list loading state
|
||||
class ListShimmerItem extends StatelessWidget {
|
||||
final double height;
|
||||
|
||||
const ListShimmerItem({
|
||||
super.key,
|
||||
this.height = 80,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: height,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 14,
|
||||
width: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
7
lib/core/widgets/widgets.dart
Normal file
7
lib/core/widgets/widgets.dart
Normal file
@@ -0,0 +1,7 @@
|
||||
// Core Reusable Widgets
|
||||
export 'loading_indicator.dart';
|
||||
export 'empty_state.dart';
|
||||
export 'error_widget.dart';
|
||||
export 'custom_button.dart';
|
||||
|
||||
// This file provides a central export point for all core widgets
|
||||
Reference in New Issue
Block a user