import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'skeleton_loader.dart'; import 'package:conduit/l10n/app_localizations.dart'; import 'improved_loading_states.dart'; /// Sliver version of an optimized list for use in CustomScrollView. class OptimizedSliverList extends ConsumerWidget { final List items; final Widget Function(BuildContext context, T item, int index) itemBuilder; final Widget? loadingWidget; final Widget? emptyWidget; final String? emptyMessage; final bool isLoading; final bool hasMore; final VoidCallback? onLoadMore; final bool addAutomaticKeepAlives; final bool addRepaintBoundaries; const OptimizedSliverList({ super.key, required this.items, required this.itemBuilder, this.loadingWidget, this.emptyWidget, this.emptyMessage, this.isLoading = false, this.hasMore = false, this.onLoadMore, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, }); @override Widget build(BuildContext context, WidgetRef ref) { // Loading state if (isLoading && items.isEmpty) { return SliverToBoxAdapter( child: loadingWidget ?? _buildDefaultLoadingWidget(), ); } // Empty state if (items.isEmpty) { return SliverToBoxAdapter( child: emptyWidget ?? Builder( builder: (context) { final l10n = AppLocalizations.of(context)!; return ImprovedEmptyState( title: l10n.noItems, subtitle: emptyMessage ?? l10n.noItemsToDisplay, icon: Icons.inbox_outlined, ); }, ), ); } // List content return SliverList( delegate: SliverChildBuilderDelegate( (context, index) { if (index >= items.length) { if (hasMore) { // Trigger pagination once this placeholder is built WidgetsBinding.instance.addPostFrameCallback((_) { onLoadMore?.call(); }); return Container( padding: const EdgeInsets.all(16.0), alignment: Alignment.center, child: const CircularProgressIndicator(), ); } return null; } final item = items[index]; final widget = itemBuilder(context, item, index); // Wrap in repaint boundary for perf if (addRepaintBoundaries) { return RepaintBoundary(child: widget); } return widget; }, childCount: items.length + (hasMore ? 1 : 0), addAutomaticKeepAlives: addAutomaticKeepAlives, addRepaintBoundaries: addRepaintBoundaries, ), ); } Widget _buildDefaultLoadingWidget() { return Column( children: List.generate( 5, (index) => const Padding( padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: SkeletonLoader(height: 80), ), ), ); } } /// Animated list with lightweight add/remove animations. class OptimizedAnimatedList extends ConsumerStatefulWidget { final List items; final Widget Function( BuildContext context, T item, int index, Animation animation, ) itemBuilder; final Duration animationDuration; final Curve animationCurve; final EdgeInsetsGeometry? padding; final ScrollController? scrollController; final bool shrinkWrap; const OptimizedAnimatedList({ super.key, required this.items, required this.itemBuilder, this.animationDuration = const Duration(milliseconds: 300), this.animationCurve = Curves.easeInOut, this.padding, this.scrollController, this.shrinkWrap = false, }); @override ConsumerState> createState() => _OptimizedAnimatedListState(); } class _OptimizedAnimatedListState extends ConsumerState> { final GlobalKey _listKey = GlobalKey(); late List _items; @override void initState() { super.initState(); _items = List.from(widget.items); } @override void didUpdateWidget(OptimizedAnimatedList oldWidget) { super.didUpdateWidget(oldWidget); // Additions for (int i = 0; i < widget.items.length; i++) { if (i >= _items.length || widget.items[i] != _items[i]) { _items.insert(i, widget.items[i]); _listKey.currentState?.insertItem( i, duration: widget.animationDuration, ); } } // Removals for (int i = _items.length - 1; i >= widget.items.length; i--) { final removedItem = _items[i]; _items.removeAt(i); _listKey.currentState?.removeItem( i, (context, animation) => widget.itemBuilder(context, removedItem, i, animation), duration: widget.animationDuration, ); } } @override Widget build(BuildContext context) { return AnimatedList( key: _listKey, controller: widget.scrollController, padding: widget.padding, shrinkWrap: widget.shrinkWrap, initialItemCount: _items.length, itemBuilder: (context, index, animation) { if (index >= _items.length) return const SizedBox.shrink(); return widget.itemBuilder(context, _items[index], index, animation); }, ); } }