sliver_reordarable_animated_list 0.1.0 copy "sliver_reordarable_animated_list: ^0.1.0" to clipboard
sliver_reordarable_animated_list: ^0.1.0 copied to clipboard

A sliver-compatible reorderable list with built-in animation helpers for Flutter.

example/lib/main.dart

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,
        ),
      ),
    );
  }
}
0
likes
135
points
101
downloads

Documentation

API reference

Publisher

verified publisherpetrovskyi.net

Weekly Downloads

A sliver-compatible reorderable list with built-in animation helpers for Flutter.

Homepage
Repository (GitHub)
View/report issues

Topics

#flutter #sliver #reorderable #list #animation

License

BSD-3-Clause, MIT (license)

Dependencies

flutter

More

Packages that depend on sliver_reordarable_animated_list