sliver_reordarable_animated_list 0.1.0
sliver_reordarable_animated_list: ^0.1.0 copied to clipboard
A sliver-compatible reorderable list with built-in animation helpers for Flutter.
import 'package:flutter/material.dart';
import 'package:sliver_reordarable_animated_list/sliver_reordarable_animated_list.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatelessWidget {
const ExampleApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Task Board',
theme: ThemeData(
colorSchemeSeed: const Color(0xFF6750A4),
useMaterial3: true,
),
home: const TaskBoardPage(),
);
}
}
// ---------------------------------------------------------------------------
// Model
// ---------------------------------------------------------------------------
enum Priority { low, medium, high }
class Task {
Task({
required this.id,
required this.title,
required this.subtitle,
required this.priority,
required this.avatarColor,
required this.avatarInitials,
this.tagLabel,
});
final int id;
final String title;
final String subtitle;
final Priority priority;
final Color avatarColor;
final String avatarInitials;
final String? tagLabel;
}
// ---------------------------------------------------------------------------
// Seed data
// ---------------------------------------------------------------------------
final List<Task> _seedTasks = [
Task(
id: 1,
title: 'Design system audit',
subtitle: 'Review all components for M3 compliance',
priority: Priority.high,
avatarColor: const Color(0xFF6750A4),
avatarInitials: 'AL',
tagLabel: 'Design',
),
Task(
id: 2,
title: 'Fix login regression',
subtitle: 'Token refresh fails on Android 12',
priority: Priority.high,
avatarColor: const Color(0xFFB3261E),
avatarInitials: 'MK',
tagLabel: 'Bug',
),
Task(
id: 3,
title: 'Write release notes',
subtitle: 'Cover new features and breaking changes',
priority: Priority.medium,
avatarColor: const Color(0xFF386A20),
avatarInitials: 'SR',
tagLabel: 'Docs',
),
Task(
id: 4,
title: 'Performance profiling',
subtitle: 'Identify jank in the feed scroll path',
priority: Priority.medium,
avatarColor: const Color(0xFF00639B),
avatarInitials: 'TN',
tagLabel: 'Perf',
),
Task(
id: 5,
title: 'Add dark mode support',
subtitle: 'Implement dynamic color theming end-to-end',
priority: Priority.low,
avatarColor: const Color(0xFF7B4F00),
avatarInitials: 'JD',
tagLabel: 'Feature',
),
Task(
id: 6,
title: 'Localise onboarding screens',
subtitle: 'Translations pending for FR, DE, JA',
priority: Priority.low,
avatarColor: const Color(0xFF006A6A),
avatarInitials: 'EP',
tagLabel: 'i18n',
),
Task(
id: 7,
title: 'Set up CI matrix',
subtitle: 'Run tests on iOS 16, 17 and Android 12-14',
priority: Priority.medium,
avatarColor: const Color(0xFF4A4458),
avatarInitials: 'OW',
tagLabel: 'DevOps',
),
];
// Pool of tasks to "add" during the demo
final List<Task> _taskPool = [
Task(
id: 201,
title: 'Accessibility review',
subtitle: 'Verify all touch targets meet 48 dp minimum',
priority: Priority.high,
avatarColor: const Color(0xFF9C4146),
avatarInitials: 'RB',
tagLabel: 'A11y',
),
Task(
id: 202,
title: 'Update dependencies',
subtitle: 'Bump Flutter SDK and third-party packages',
priority: Priority.low,
avatarColor: const Color(0xFF2E5339),
avatarInitials: 'CG',
tagLabel: 'Maint',
),
Task(
id: 203,
title: 'API rate-limit handling',
subtitle: 'Show graceful UI when 429 responses arrive',
priority: Priority.medium,
avatarColor: const Color(0xFF004E73),
avatarInitials: 'YL',
tagLabel: 'Backend',
),
Task(
id: 204,
title: 'Crash reporting integration',
subtitle: 'Wire Sentry to production build flavour',
priority: Priority.high,
avatarColor: const Color(0xFF6B3A2A),
avatarInitials: 'HF',
tagLabel: 'Ops',
),
];
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
class TaskBoardPage extends StatefulWidget {
const TaskBoardPage({Key? key}) : super(key: key);
@override
State<TaskBoardPage> createState() => _TaskBoardPageState();
}
class _TaskBoardPageState extends State<TaskBoardPage> {
final _listKey = GlobalKey<SliverReorderableAnimatedListState>();
late final List<Task> _tasks;
int _poolIndex = 0;
@override
void initState() {
super.initState();
_tasks = List.of(_seedTasks);
}
// -------------------------------------------------------------------------
// Insert
// -------------------------------------------------------------------------
void _addTask() {
if (_poolIndex >= _taskPool.length) return;
final task = _taskPool[_poolIndex++];
const insertAt = 0; // always insert at top
setState(() => _tasks.insert(insertAt, task));
_listKey.currentState?.insertItem(insertAt);
}
// -------------------------------------------------------------------------
// Remove
// -------------------------------------------------------------------------
void _removeTask(int index) {
final task = _tasks[index];
_listKey.currentState?.removeItem(
index,
(context, animation) => _buildTaskCard(
context,
task,
index,
animation,
removing: true,
),
);
setState(() => _tasks.removeAt(index));
}
// -------------------------------------------------------------------------
// Reorder
// -------------------------------------------------------------------------
void _onReorder(int oldIndex, int newIndex) {
setState(() {
final item = _tasks.removeAt(oldIndex);
_tasks.insert(newIndex > oldIndex ? newIndex - 1 : newIndex, item);
});
}
// -------------------------------------------------------------------------
// Build
// -------------------------------------------------------------------------
@override
Widget build(BuildContext context) {
final canAdd = _poolIndex < _taskPool.length;
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest,
body: CustomScrollView(
slivers: [
_buildAppBar(context),
SliverPadding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 120),
sliver: SliverReorderableAnimatedList(
key: _listKey,
initialItemsCount: _tasks.length,
itemBuilder: (context, index, animation) {
final task = _tasks[index];
return _buildTaskCard(context, task, index, animation);
},
onReorder: _onReorder,
),
),
],
),
floatingActionButton: canAdd
? FloatingActionButton.extended(
onPressed: _addTask,
icon: const Icon(Icons.add_task),
label: const Text('Add task'),
)
: null,
);
}
SliverAppBar _buildAppBar(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return SliverAppBar(
pinned: true,
expandedHeight: 120,
backgroundColor: scheme.surface,
flexibleSpace: FlexibleSpaceBar(
title: Text(
'Task Board',
style: TextStyle(color: scheme.onSurface, fontWeight: FontWeight.w700),
),
titlePadding: const EdgeInsets.only(left: 20, bottom: 16),
background: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
scheme.primaryContainer,
scheme.surface,
],
),
),
),
),
);
}
// -------------------------------------------------------------------------
// Task card
// -------------------------------------------------------------------------
Widget _buildTaskCard(
BuildContext context,
Task task,
int index,
Animation<double> animation, {
bool removing = false,
}) {
return SizeTransition(
key: ValueKey(task.id),
sizeFactor: animation,
child: FadeTransition(
opacity: animation,
// Key must live on the outermost widget returned by itemBuilder.
child: AnimatedReorderableDelayedDragStartListener(
index: index,
enabled: !removing,
child: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _TaskCard(
task: task,
onDelete: removing ? null : () => _removeTask(index),
),
),
),
),
);
}
}
// ---------------------------------------------------------------------------
// Task card widget
// ---------------------------------------------------------------------------
class _TaskCard extends StatelessWidget {
const _TaskCard({required this.task, this.onDelete});
final Task task;
final VoidCallback? onDelete;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final Color priorityColor;
final String priorityLabel;
switch (task.priority) {
case Priority.high:
priorityColor = const Color(0xFFB3261E);
priorityLabel = 'High';
break;
case Priority.medium:
priorityColor = const Color(0xFF7B4F00);
priorityLabel = 'Medium';
break;
case Priority.low:
priorityColor = const Color(0xFF386A20);
priorityLabel = 'Low';
break;
}
return Material(
borderRadius: BorderRadius.circular(16),
color: scheme.surface,
elevation: 1,
shadowColor: scheme.shadow.withValues(alpha: 0.3),
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Header row ──────────────────────────────────────────────
Row(
children: [
// Drag handle
Icon(Icons.drag_indicator,
size: 20, color: scheme.onSurfaceVariant.withValues(alpha: 0.5)),
const SizedBox(width: 8),
// Avatar
CircleAvatar(
radius: 16,
backgroundColor: task.avatarColor,
child: Text(
task.avatarInitials,
style: const TextStyle(
color: Colors.white,
fontSize: 11,
fontWeight: FontWeight.w700),
),
),
const SizedBox(width: 10),
// Title
Expanded(
child: Text(
task.title,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w700,
color: scheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// Delete button
if (onDelete != null)
IconButton(
icon: Icon(Icons.close_rounded,
size: 18, color: scheme.onSurfaceVariant),
onPressed: onDelete,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
visualDensity: VisualDensity.compact,
),
],
),
const SizedBox(height: 8),
// ── Subtitle ─────────────────────────────────────────────────
Padding(
padding: const EdgeInsets.only(left: 44),
child: Text(
task.subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(height: 10),
// ── Footer row ───────────────────────────────────────────────
Padding(
padding: const EdgeInsets.only(left: 44),
child: Row(
children: [
// Priority chip
_Chip(
label: priorityLabel,
color: priorityColor,
),
if (task.tagLabel != null) ...[
const SizedBox(width: 6),
_Chip(
label: task.tagLabel!,
color: scheme.primary,
),
],
],
),
),
],
),
),
);
}
}
class _Chip extends StatelessWidget {
const _Chip({required this.label, required this.color});
final String label;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: color.withValues(alpha: 0.4), width: 0.8),
),
child: Text(
label,
style: TextStyle(
color: color,
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
);
}
}