Refactoring
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import '../../../core/performance/performance_optimizer.dart';
|
||||
|
||||
/// ListView optimisé avec lazy loading intelligent et gestion de performance
|
||||
///
|
||||
/// Fonctionnalités :
|
||||
/// - Lazy loading avec seuil configurable
|
||||
/// - Recyclage automatique des widgets
|
||||
/// - Animations optimisées
|
||||
/// - Gestion mémoire intelligente
|
||||
/// - Monitoring des performances
|
||||
class OptimizedListView<T> extends StatefulWidget {
|
||||
/// Liste des éléments à afficher
|
||||
final List<T> items;
|
||||
|
||||
/// Builder pour chaque élément
|
||||
final Widget Function(BuildContext context, T item, int index) itemBuilder;
|
||||
|
||||
/// Callback pour charger plus d'éléments
|
||||
final Future<void> Function()? onLoadMore;
|
||||
|
||||
/// Callback pour rafraîchir la liste
|
||||
final Future<void> Function()? onRefresh;
|
||||
|
||||
/// Indique si plus d'éléments peuvent être chargés
|
||||
final bool hasMore;
|
||||
|
||||
/// Indique si le chargement est en cours
|
||||
final bool isLoading;
|
||||
|
||||
/// Seuil pour déclencher le chargement (nombre d'éléments avant la fin)
|
||||
final int loadMoreThreshold;
|
||||
|
||||
/// Hauteur estimée d'un élément (pour l'optimisation)
|
||||
final double? itemExtent;
|
||||
|
||||
/// Padding de la liste
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// Séparateur entre les éléments
|
||||
final Widget? separator;
|
||||
|
||||
/// Widget affiché quand la liste est vide
|
||||
final Widget? emptyWidget;
|
||||
|
||||
/// Widget de chargement personnalisé
|
||||
final Widget? loadingWidget;
|
||||
|
||||
/// Activer les animations
|
||||
final bool enableAnimations;
|
||||
|
||||
/// Durée des animations
|
||||
final Duration animationDuration;
|
||||
|
||||
/// Contrôleur de scroll personnalisé
|
||||
final ScrollController? scrollController;
|
||||
|
||||
/// Physics du scroll
|
||||
final ScrollPhysics? physics;
|
||||
|
||||
/// Activer le recyclage des widgets
|
||||
final bool enableRecycling;
|
||||
|
||||
/// Nombre maximum de widgets en cache
|
||||
final int maxCachedWidgets;
|
||||
|
||||
const OptimizedListView({
|
||||
super.key,
|
||||
required this.items,
|
||||
required this.itemBuilder,
|
||||
this.onLoadMore,
|
||||
this.onRefresh,
|
||||
this.hasMore = true,
|
||||
this.isLoading = false,
|
||||
this.loadMoreThreshold = 3,
|
||||
this.itemExtent,
|
||||
this.padding,
|
||||
this.separator,
|
||||
this.emptyWidget,
|
||||
this.loadingWidget,
|
||||
this.enableAnimations = true,
|
||||
this.animationDuration = const Duration(milliseconds: 300),
|
||||
this.scrollController,
|
||||
this.physics,
|
||||
this.enableRecycling = true,
|
||||
this.maxCachedWidgets = 50,
|
||||
});
|
||||
|
||||
@override
|
||||
State<OptimizedListView<T>> createState() => _OptimizedListViewState<T>();
|
||||
}
|
||||
|
||||
class _OptimizedListViewState<T> extends State<OptimizedListView<T>>
|
||||
with TickerProviderStateMixin {
|
||||
|
||||
late ScrollController _scrollController;
|
||||
late AnimationController _animationController;
|
||||
|
||||
/// Cache des widgets recyclés
|
||||
final Map<String, Widget> _widgetCache = {};
|
||||
|
||||
/// Performance optimizer instance
|
||||
final _optimizer = PerformanceOptimizer();
|
||||
|
||||
/// Indique si le chargement est en cours
|
||||
bool _isLoadingMore = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_scrollController = widget.scrollController ?? ScrollController();
|
||||
_animationController = PerformanceOptimizer.createOptimizedController(
|
||||
duration: widget.animationDuration,
|
||||
vsync: this,
|
||||
debugLabel: 'OptimizedListView',
|
||||
);
|
||||
|
||||
// Écouter le scroll pour le lazy loading
|
||||
_scrollController.addListener(_onScroll);
|
||||
|
||||
// Démarrer les animations si activées
|
||||
if (widget.enableAnimations) {
|
||||
_animationController.forward();
|
||||
}
|
||||
|
||||
_optimizer.startTimer('list_build');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (widget.scrollController == null) {
|
||||
_scrollController.dispose();
|
||||
}
|
||||
_animationController.dispose();
|
||||
_widgetCache.clear();
|
||||
_optimizer.stopTimer('list_build');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
|
||||
final position = _scrollController.position;
|
||||
final maxScroll = position.maxScrollExtent;
|
||||
final currentScroll = position.pixels;
|
||||
|
||||
// Calculer si on approche de la fin
|
||||
final threshold = maxScroll - (widget.loadMoreThreshold * (widget.itemExtent ?? 100));
|
||||
|
||||
if (currentScroll >= threshold &&
|
||||
widget.hasMore &&
|
||||
!_isLoadingMore &&
|
||||
widget.onLoadMore != null) {
|
||||
_loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMore() async {
|
||||
if (_isLoadingMore) return;
|
||||
|
||||
setState(() {
|
||||
_isLoadingMore = true;
|
||||
});
|
||||
|
||||
_optimizer.startTimer('load_more');
|
||||
|
||||
try {
|
||||
await widget.onLoadMore!();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoadingMore = false;
|
||||
});
|
||||
}
|
||||
_optimizer.stopTimer('load_more');
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOptimizedItem(BuildContext context, int index) {
|
||||
if (index >= widget.items.length) {
|
||||
// Widget de chargement en fin de liste
|
||||
return _buildLoadingIndicator();
|
||||
}
|
||||
|
||||
final item = widget.items[index];
|
||||
final cacheKey = 'item_${item.hashCode}_$index';
|
||||
|
||||
// Utiliser le cache si le recyclage est activé
|
||||
if (widget.enableRecycling && _widgetCache.containsKey(cacheKey)) {
|
||||
_optimizer.incrementCounter('cache_hit');
|
||||
return _widgetCache[cacheKey]!;
|
||||
}
|
||||
|
||||
// Construire le widget
|
||||
Widget itemWidget = widget.itemBuilder(context, item, index);
|
||||
|
||||
// Optimiser le widget
|
||||
itemWidget = PerformanceOptimizer.optimizeWidget(
|
||||
itemWidget,
|
||||
key: 'optimized_$index',
|
||||
forceRepaintBoundary: true,
|
||||
);
|
||||
|
||||
// Ajouter les animations si activées
|
||||
if (widget.enableAnimations) {
|
||||
itemWidget = _buildAnimatedItem(itemWidget, index);
|
||||
}
|
||||
|
||||
// Mettre en cache si le recyclage est activé
|
||||
if (widget.enableRecycling) {
|
||||
_cacheWidget(cacheKey, itemWidget);
|
||||
}
|
||||
|
||||
_optimizer.incrementCounter('item_built');
|
||||
return itemWidget;
|
||||
}
|
||||
|
||||
Widget _buildAnimatedItem(Widget child, int index) {
|
||||
final delay = Duration(milliseconds: (index * 50).clamp(0, 500));
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: _animationController,
|
||||
builder: (context, _) {
|
||||
final animationValue = Curves.easeOutCubic.transform(
|
||||
(_animationController.value - (delay.inMilliseconds / widget.animationDuration.inMilliseconds))
|
||||
.clamp(0.0, 1.0),
|
||||
);
|
||||
|
||||
return Transform.translate(
|
||||
offset: Offset(0, 50 * (1 - animationValue)),
|
||||
child: Opacity(
|
||||
opacity: animationValue,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _cacheWidget(String key, Widget widget) {
|
||||
// Limiter la taille du cache
|
||||
if (_widgetCache.length >= widget.maxCachedWidgets) {
|
||||
// Supprimer les plus anciens (simple FIFO)
|
||||
final oldestKey = _widgetCache.keys.first;
|
||||
_widgetCache.remove(oldestKey);
|
||||
}
|
||||
|
||||
_widgetCache[key] = widget;
|
||||
}
|
||||
|
||||
Widget _buildLoadingIndicator() {
|
||||
return widget.loadingWidget ??
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState() {
|
||||
return widget.emptyWidget ??
|
||||
const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.inbox_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Aucun élément à afficher',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Liste vide
|
||||
if (widget.items.isEmpty && !widget.isLoading) {
|
||||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
// Calculer le nombre total d'éléments (items + indicateur de chargement)
|
||||
final itemCount = widget.items.length + (widget.hasMore && _isLoadingMore ? 1 : 0);
|
||||
|
||||
Widget listView;
|
||||
|
||||
if (widget.separator != null) {
|
||||
// ListView avec séparateurs
|
||||
listView = ListView.separated(
|
||||
controller: _scrollController,
|
||||
physics: widget.physics,
|
||||
padding: widget.padding,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: _buildOptimizedItem,
|
||||
separatorBuilder: (context, index) => widget.separator!,
|
||||
);
|
||||
} else {
|
||||
// ListView standard
|
||||
listView = ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: widget.physics,
|
||||
padding: widget.padding,
|
||||
itemCount: itemCount,
|
||||
itemExtent: widget.itemExtent,
|
||||
itemBuilder: _buildOptimizedItem,
|
||||
);
|
||||
}
|
||||
|
||||
// Ajouter RefreshIndicator si onRefresh est fourni
|
||||
if (widget.onRefresh != null) {
|
||||
listView = RefreshIndicator(
|
||||
onRefresh: widget.onRefresh!,
|
||||
child: listView,
|
||||
);
|
||||
}
|
||||
|
||||
return listView;
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension pour faciliter l'utilisation
|
||||
extension OptimizedListViewExtension<T> on List<T> {
|
||||
/// Crée un OptimizedListView à partir de cette liste
|
||||
Widget toOptimizedListView({
|
||||
required Widget Function(BuildContext context, T item, int index) itemBuilder,
|
||||
Future<void> Function()? onLoadMore,
|
||||
Future<void> Function()? onRefresh,
|
||||
bool hasMore = false,
|
||||
bool isLoading = false,
|
||||
int loadMoreThreshold = 3,
|
||||
double? itemExtent,
|
||||
EdgeInsetsGeometry? padding,
|
||||
Widget? separator,
|
||||
Widget? emptyWidget,
|
||||
Widget? loadingWidget,
|
||||
bool enableAnimations = true,
|
||||
Duration animationDuration = const Duration(milliseconds: 300),
|
||||
ScrollController? scrollController,
|
||||
ScrollPhysics? physics,
|
||||
bool enableRecycling = true,
|
||||
int maxCachedWidgets = 50,
|
||||
}) {
|
||||
return OptimizedListView<T>(
|
||||
items: this,
|
||||
itemBuilder: itemBuilder,
|
||||
onLoadMore: onLoadMore,
|
||||
onRefresh: onRefresh,
|
||||
hasMore: hasMore,
|
||||
isLoading: isLoading,
|
||||
loadMoreThreshold: loadMoreThreshold,
|
||||
itemExtent: itemExtent,
|
||||
padding: padding,
|
||||
separator: separator,
|
||||
emptyWidget: emptyWidget,
|
||||
loadingWidget: loadingWidget,
|
||||
enableAnimations: enableAnimations,
|
||||
animationDuration: animationDuration,
|
||||
scrollController: scrollController,
|
||||
physics: physics,
|
||||
enableRecycling: enableRecycling,
|
||||
maxCachedWidgets: maxCachedWidgets,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user