Refactoring

This commit is contained in:
DahoudG
2025-09-17 17:54:06 +00:00
parent 12d514d866
commit 63fe107f98
165 changed files with 54220 additions and 276 deletions

View File

@@ -0,0 +1,371 @@
import 'package:flutter/material.dart';
import '../../theme/app_theme.dart';
import '../cards/unified_card_widget.dart';
/// Widget de liste unifié avec animations et gestion d'états
///
/// Fournit :
/// - Animations d'apparition staggerées
/// - Gestion du scroll infini
/// - États de chargement et d'erreur
/// - Refresh-to-reload
/// - Séparateurs personnalisables
class UnifiedListWidget<T> extends StatefulWidget {
/// Liste des éléments à afficher
final List<T> items;
/// Builder pour chaque élément de la liste
final Widget Function(BuildContext context, T item, int index) itemBuilder;
/// Indique si la liste est en cours de chargement
final bool isLoading;
/// Indique si tous les éléments ont été chargés (pour le scroll infini)
final bool hasReachedMax;
/// Callback pour charger plus d'éléments
final VoidCallback? onLoadMore;
/// Callback pour rafraîchir la liste
final Future<void> Function()? onRefresh;
/// Message d'erreur à afficher
final String? errorMessage;
/// Callback pour réessayer en cas d'erreur
final VoidCallback? onRetry;
/// Widget à afficher quand la liste est vide
final Widget? emptyWidget;
/// Message à afficher quand la liste est vide
final String? emptyMessage;
/// Icône à afficher quand la liste est vide
final IconData? emptyIcon;
/// Padding de la liste
final EdgeInsetsGeometry? padding;
/// Espacement entre les éléments
final double itemSpacing;
/// Indique si les animations d'apparition sont activées
final bool enableAnimations;
/// Durée de l'animation d'apparition de chaque élément
final Duration animationDuration;
/// Délai entre les animations d'éléments
final Duration animationDelay;
/// Contrôleur de scroll personnalisé
final ScrollController? scrollController;
/// Physics du scroll
final ScrollPhysics? physics;
const UnifiedListWidget({
super.key,
required this.items,
required this.itemBuilder,
this.isLoading = false,
this.hasReachedMax = false,
this.onLoadMore,
this.onRefresh,
this.errorMessage,
this.onRetry,
this.emptyWidget,
this.emptyMessage,
this.emptyIcon,
this.padding,
this.itemSpacing = 12.0,
this.enableAnimations = true,
this.animationDuration = const Duration(milliseconds: 300),
this.animationDelay = const Duration(milliseconds: 100),
this.scrollController,
this.physics,
});
@override
State<UnifiedListWidget<T>> createState() => _UnifiedListWidgetState<T>();
}
class _UnifiedListWidgetState<T> extends State<UnifiedListWidget<T>>
with TickerProviderStateMixin {
late ScrollController _scrollController;
late AnimationController _listAnimationController;
List<AnimationController> _itemControllers = [];
List<Animation<double>> _itemAnimations = [];
List<Animation<Offset>> _slideAnimations = [];
@override
void initState() {
super.initState();
_scrollController = widget.scrollController ?? ScrollController();
_scrollController.addListener(_onScroll);
_listAnimationController = AnimationController(
duration: const Duration(milliseconds: 600),
vsync: this,
);
_initializeItemAnimations();
if (widget.enableAnimations) {
_startAnimations();
}
}
@override
void didUpdateWidget(UnifiedListWidget<T> oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.items.length != oldWidget.items.length) {
_updateItemAnimations();
}
}
@override
void dispose() {
if (widget.scrollController == null) {
_scrollController.dispose();
}
_listAnimationController.dispose();
for (final controller in _itemControllers) {
controller.dispose();
}
super.dispose();
}
void _initializeItemAnimations() {
if (!widget.enableAnimations) return;
_updateItemAnimations();
}
void _updateItemAnimations() {
if (!widget.enableAnimations) return;
// Dispose des anciens controllers s'ils existent
if (_itemControllers.isNotEmpty) {
for (final controller in _itemControllers) {
controller.dispose();
}
}
// Créer de nouveaux controllers pour chaque élément
_itemControllers = List.generate(
widget.items.length,
(index) => AnimationController(
duration: widget.animationDuration,
vsync: this,
),
);
// Animations de fade et scale
_itemAnimations = _itemControllers.map((controller) {
return Tween<double>(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
),
);
}).toList();
// Animations de slide depuis le bas
_slideAnimations = _itemControllers.map((controller) {
return Tween<Offset>(
begin: const Offset(0, 0.3),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
),
);
}).toList();
}
void _startAnimations() {
if (!widget.enableAnimations) return;
_listAnimationController.forward();
// Démarrer les animations des éléments avec un délai
for (int i = 0; i < _itemControllers.length; i++) {
Future.delayed(widget.animationDelay * i, () {
if (mounted && i < _itemControllers.length) {
_itemControllers[i].forward();
}
});
}
}
void _onScroll() {
if (_isBottom && widget.onLoadMore != null && !widget.isLoading && !widget.hasReachedMax) {
widget.onLoadMore!();
}
}
bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9);
}
@override
Widget build(BuildContext context) {
// Gestion de l'état d'erreur
if (widget.errorMessage != null) {
return _buildErrorState();
}
// Gestion de l'état vide
if (widget.items.isEmpty && !widget.isLoading) {
return widget.emptyWidget ?? _buildEmptyState();
}
Widget listView = ListView.separated(
controller: _scrollController,
physics: widget.physics ?? const AlwaysScrollableScrollPhysics(),
padding: widget.padding ?? const EdgeInsets.all(16),
itemCount: widget.items.length + (widget.isLoading ? 1 : 0),
separatorBuilder: (context, index) => SizedBox(height: widget.itemSpacing),
itemBuilder: (context, index) {
// Indicateur de chargement en bas de liste
if (index >= widget.items.length) {
return _buildLoadingIndicator();
}
final item = widget.items[index];
Widget itemWidget = widget.itemBuilder(context, item, index);
// Application des animations si activées
if (widget.enableAnimations && index < _itemAnimations.length) {
itemWidget = AnimatedBuilder(
animation: _itemAnimations[index],
builder: (context, child) {
return FadeTransition(
opacity: _itemAnimations[index],
child: SlideTransition(
position: _slideAnimations[index],
child: Transform.scale(
scale: 0.8 + (0.2 * _itemAnimations[index].value),
child: child,
),
),
);
},
child: itemWidget,
);
}
return itemWidget;
},
);
// Ajout du RefreshIndicator si onRefresh est fourni
if (widget.onRefresh != null) {
listView = RefreshIndicator(
onRefresh: widget.onRefresh!,
child: listView,
);
}
return listView;
}
Widget _buildLoadingIndicator() {
return const Padding(
padding: EdgeInsets.all(16.0),
child: Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
),
),
);
}
Widget _buildEmptyState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
size: 64,
color: AppTheme.textSecondary.withOpacity(0.5),
),
const SizedBox(height: 16),
Text(
'Aucun élément',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: AppTheme.textSecondary.withOpacity(0.7),
),
),
const SizedBox(height: 8),
Text(
'La liste est vide pour le moment',
style: TextStyle(
fontSize: 14,
color: AppTheme.textSecondary.withOpacity(0.5),
),
),
],
),
),
);
}
Widget _buildErrorState() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: AppTheme.errorColor,
),
const SizedBox(height: 16),
const Text(
'Une erreur est survenue',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 8),
Text(
widget.errorMessage!,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 24),
if (widget.onRetry != null)
ElevatedButton.icon(
onPressed: widget.onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
),
],
),
),
);
}
}