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

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

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

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

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

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

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

View 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