This commit is contained in:
Phuoc Nguyen
2025-10-10 16:38:07 +07:00
parent e5b247d622
commit b94c158004
177 changed files with 25080 additions and 152 deletions

View 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),
),
),
],
),
),
),
],
),
);
}
}