Files
iiEsaywebUIapp/lib/shared/widgets/optimized_list.dart

195 lines
5.4 KiB
Dart
Raw Permalink Normal View History

2025-08-10 01:20:45 +05:30
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'skeleton_loader.dart';
import 'package:conduit/l10n/app_localizations.dart';
2025-08-10 01:20:45 +05:30
import 'improved_loading_states.dart';
/// Sliver version of an optimized list for use in CustomScrollView.
2025-08-10 01:20:45 +05:30
class OptimizedSliverList<T> extends ConsumerWidget {
final List<T> 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
2025-08-10 01:20:45 +05:30
if (isLoading && items.isEmpty) {
return SliverToBoxAdapter(
child: loadingWidget ?? _buildDefaultLoadingWidget(),
);
}
// Empty state
2025-08-10 01:20:45 +05:30
if (items.isEmpty) {
return SliverToBoxAdapter(
child:
emptyWidget ??
2025-09-24 12:00:49 +05:30
Builder(
builder: (context) {
final l10n = AppLocalizations.of(context)!;
return ImprovedEmptyState(
title: l10n.noItems,
subtitle: emptyMessage ?? l10n.noItemsToDisplay,
icon: Icons.inbox_outlined,
);
},
),
2025-08-10 01:20:45 +05:30
);
}
// List content
2025-08-10 01:20:45 +05:30
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index >= items.length) {
if (hasMore) {
// Trigger pagination once this placeholder is built
2025-08-10 01:20:45 +05:30
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
2025-08-10 01:20:45 +05:30
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.
2025-08-10 01:20:45 +05:30
class OptimizedAnimatedList<T> extends ConsumerStatefulWidget {
final List<T> items;
final Widget Function(
BuildContext context,
T item,
int index,
Animation<double> 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<OptimizedAnimatedList<T>> createState() =>
_OptimizedAnimatedListState<T>();
}
class _OptimizedAnimatedListState<T>
extends ConsumerState<OptimizedAnimatedList<T>> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
late List<T> _items;
@override
void initState() {
super.initState();
_items = List.from(widget.items);
}
@override
void didUpdateWidget(OptimizedAnimatedList<T> oldWidget) {
super.didUpdateWidget(oldWidget);
// Additions
2025-08-10 01:20:45 +05:30
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
2025-08-10 01:20:45 +05:30
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);
},
);
}
}