multi_reorderable 0.2.2 copy "multi_reorderable: ^0.2.2" to clipboard
multi_reorderable: ^0.2.2 copied to clipboard

A Flutter package for multi-selection and animated reordering of items.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:multi_reorderable/multi_reorderable.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Reorderable Multi Select Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.light,
        ),
      ),
      darkTheme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.blue,
          brightness: Brightness.dark,
        ),
      ),
      themeMode: ThemeMode.system,
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatefulWidget {
  const HomeScreen({super.key});

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  int _currentIndex = 0;

  final List<Widget> _screens = [
    const ExampleScreen(),
    const AdvancedExampleScreen(),
    const PaginationExampleScreen(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: _screens[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _currentIndex,
        onTap: (index) {
          setState(() {
            _currentIndex = index;
          });
        },
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.list),
            label: 'Basic Example',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.list_alt),
            label: 'Advanced Example',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.data_array),
            label: 'Pagination',
          ),
        ],
      ),
    );
  }
}

class ExampleScreen extends StatefulWidget {
  const ExampleScreen({super.key});

  @override
  State<ExampleScreen> createState() => _ExampleScreenState();
}

class _ExampleScreenState extends State<ExampleScreen> {
  // Sample data
  List<ItemData> items = List.generate(
    20,
    (index) => ItemData(
      id: 'item_$index',
      title: 'Item ${index + 1}',
      subtitle: 'Description for item ${index + 1}',
      color: Colors.primaries[index % Colors.primaries.length],
    ),
  );

  // Selected items
  List<ItemData> selectedItems = [];

  // Current drag style
  DragStyle _currentDragStyle = DragStyle.stackedStyle;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Basic Example'),
      ),
      body: Column(
        children: [
          // Drag style selector
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              children: [
                const Text('Drag Style:',
                    style: TextStyle(fontWeight: FontWeight.bold)),
                const SizedBox(width: 16),
                Expanded(
                  child: DropdownButton<DragStyle>(
                    value: _currentDragStyle,
                    isExpanded: true,
                    onChanged: (DragStyle? newValue) {
                      if (newValue != null) {
                        setState(() {
                          _currentDragStyle = newValue;
                        });
                      }
                    },
                    items: DragStyle.values
                        .map<DropdownMenuItem<DragStyle>>((DragStyle style) {
                      return DropdownMenuItem<DragStyle>(
                        value: style,
                        child: Text(_getDragStyleDisplayName(style)),
                      );
                    }).toList(),
                  ),
                ),
              ],
            ),
          ),
          // Basic usage example
          Expanded(
            child: ReorderableMultiDragList<ItemData>(
              items: items,
              itemBuilder: (context, item, index, isSelected, isDragging) {
                return ListTile(
                  title: Text(item.title),
                  subtitle: Text(item.subtitle),
                  leading: CircleAvatar(
                    backgroundColor: item.color,
                    child: Text('${index + 1}'),
                  ),
                );
              },
              onReorder: (newIndex, movedItems, reorderedItems) {
                setState(() {
                  items = reorderedItems;
                });
              },
              onSelectionChanged: (selected) {
                setState(() {
                  selectedItems = List.from(selected);
                });
              },
              onDone: (selected) {
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(
                    content: Text('Selected ${selected.length} items'),
                    duration: const Duration(seconds: 2),
                  ),
                );
              },
              initialSelection: selectedItems,
              showDoneButton: true,
              doneButtonText: 'Done',
              showSelectionCount: true,
              selectionCountText: 'Selected {} items',
              itemHeight: 80.0,
              showDividers: true,
              theme: ReorderableMultiDragTheme(
                draggedItemBorderColor: Theme.of(context).colorScheme.primary,
                dragStyle: _currentDragStyle,
              ),
            ),
          ),
        ],
      ),
      bottomNavigationBar: BottomAppBar(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            'Selected ${selectedItems.length} items',
            style: Theme.of(context).textTheme.titleMedium,
          ),
        ),
      ),
    );
  }

  // Helper method to get a user-friendly name for drag styles
  String _getDragStyleDisplayName(DragStyle style) {
    switch (style) {
      case DragStyle.stackedStyle:
        return 'Stacked Style';
      case DragStyle.animatedCardStyle:
        return 'Animated Cards';
      case DragStyle.minimalistStyle:
        return 'Minimalist';
    }
  }
}

// Example data model
class ItemData {
  final String id;
  final String title;
  final String subtitle;
  final Color color;

  ItemData({
    required this.id,
    required this.title,
    required this.subtitle,
    required this.color,
  });

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is ItemData && other.id == id;
  }

  @override
  int get hashCode => id.hashCode;
}

// Advanced Example Screen
class AdvancedExampleScreen extends StatefulWidget {
  const AdvancedExampleScreen({super.key});

  @override
  State<AdvancedExampleScreen> createState() => _AdvancedExampleScreenState();
}

class _AdvancedExampleScreenState extends State<AdvancedExampleScreen> {
  // Sample data
  List<TaskItem> tasks = List.generate(
    15,
    (index) => TaskItem(
      id: 'task_$index',
      title: 'Task ${index + 1}',
      description: 'Complete task ${index + 1} before deadline',
      priority: TaskPriority.values[index % TaskPriority.values.length],
      dueDate: DateTime.now().add(Duration(days: index + 1)),
      isCompleted: false,
    ),
  );

  // Selected items
  List<TaskItem> selectedTasks = [];

  // Custom theme
  late ReorderableMultiDragTheme customTheme;

  @override
  void initState() {
    super.initState();
    // Initialize with some selected tasks
    selectedTasks = [tasks[0], tasks[2]];
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    // Create custom theme based on app theme
    customTheme = ReorderableMultiDragTheme(
      draggedItemBorderColor: Theme.of(context).colorScheme.primary,
      itemBorderRadius: 12.0,
      dragStyle: DragStyle.stackedStyle,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Task Manager'),
        elevation: 0,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            // Custom header
            Container(
              padding: const EdgeInsets.all(16.0),
              decoration: BoxDecoration(
                color: Theme.of(context).colorScheme.surfaceContainerHighest,
                borderRadius: BorderRadius.circular(12.0),
              ),
              child: Row(
                children: [
                  Icon(
                    Icons.task_alt,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Text(
                      'Task Manager',
                      style: Theme.of(context).textTheme.titleLarge,
                    ),
                  ),
                  Text(
                    'Selected ${selectedTasks.length} / ${tasks.length} tasks',
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                ],
              ),
            ),
            const SizedBox(height: 16),

            // Advanced usage example with custom builders and theme
            Expanded(
              child: ReorderableMultiDragList<TaskItem>(
                items: tasks,
                itemBuilder: (context, task, index, isSelected, isDragging) {
                  return _buildTaskItem(task, index);
                },
                onReorder: (newIndex, movedItems, reorderedItems) {
                  setState(() {
                    tasks = reorderedItems;
                  });
                },
                onSelectionChanged: (selected) {
                  setState(() {
                    selectedTasks = List.from(selected);
                  });
                },
                onDone: (selected) {
                  // Mark selected tasks as completed
                  setState(() {
                    for (final task in selected) {
                      final index = tasks.indexWhere((t) => t.id == task.id);
                      if (index != -1) {
                        tasks[index] = tasks[index].copyWith(isCompleted: true);
                      }
                    }
                    selectedTasks = [];
                  });

                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(
                      content:
                          Text('Marked ${selected.length} tasks as completed'),
                      duration: const Duration(seconds: 2),
                    ),
                  );
                },
                initialSelection: selectedTasks,
                showDoneButton: false,
                doneButtonText: 'Mark as Completed',
                showSelectionCount: false,
                selectionCountText: '{} tasks selected',
                itemHeight: 100.0,
                showDividers: true,
                theme: customTheme,
                dragAnimationDuration: const Duration(milliseconds: 200),
                reorderAnimationDuration: const Duration(milliseconds: 300),
                headerWidget: _buildCustomHeader(
                    context, selectedTasks, selectedTasks.isNotEmpty),
                footerWidget: selectedTasks.isNotEmpty
                    ? _buildCustomFooter(context, selectedTasks, true,
                        () => _markTasksAsCompleted(selectedTasks))
                    : null,
                dragHandleBuilder: (context, isSelected) =>
                    _buildCustomDragHandle(context, isSelected, false),
                selectionBarBuilder: (context, selectedCount, onDone) {
                  return const SizedBox();
                },
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _addNewTask,
        child: const Icon(Icons.add),
      ),
    );
  }

  // Custom builders
  Widget _buildTaskItem(TaskItem task, int index) {
    return Card(
      margin: EdgeInsets.zero,
      elevation: 0,
      color: Colors.transparent,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              children: [
                _getPriorityIcon(task.priority),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    task.title,
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                      decoration:
                          task.isCompleted ? TextDecoration.lineThrough : null,
                      color: task.isCompleted
                          ? Theme.of(context).colorScheme.outline
                          : Theme.of(context).colorScheme.onSurface,
                    ),
                  ),
                ),
                Text(
                  _formatDate(task.dueDate),
                  style: TextStyle(
                    fontSize: 12,
                    color: _isOverdue(task.dueDate) && !task.isCompleted
                        ? Colors.red
                        : Theme.of(context).colorScheme.onSurfaceVariant,
                  ),
                ),
              ],
            ),
            const SizedBox(height: 4),
            Padding(
              padding: const EdgeInsets.only(left: 32.0),
              child: Text(
                task.description,
                style: TextStyle(
                  fontSize: 14,
                  color: task.isCompleted
                      ? Theme.of(context).colorScheme.outline
                      : Theme.of(context).colorScheme.onSurfaceVariant,
                ),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
            ),
            const SizedBox(height: 4),
            if (task.isCompleted)
              Padding(
                padding: const EdgeInsets.only(left: 32.0),
                child: Text(
                  'Completed',
                  style: TextStyle(
                    fontSize: 12,
                    fontStyle: FontStyle.italic,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  Widget _buildCustomHeader(
    BuildContext context,
    List<dynamic> selectedItems,
    bool isSelectionMode,
  ) {
    if (!isSelectionMode) return const SizedBox.shrink();

    return Container(
      padding: const EdgeInsets.all(16.0),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.primaryContainer,
        borderRadius: const BorderRadius.only(
          topLeft: Radius.circular(12.0),
          topRight: Radius.circular(12.0),
        ),
      ),
      child: Row(
        children: [
          Icon(
            Icons.check_circle,
            color: Theme.of(context).colorScheme.primary,
          ),
          const SizedBox(width: 12),
          Text(
            'Selection Mode',
            style: Theme.of(context).textTheme.titleMedium!.copyWith(
                  fontWeight: FontWeight.bold,
                ),
          ),
        ],
      ),
    );
  }

  Widget _buildCustomFooter(
    BuildContext context,
    List<dynamic> selectedItems,
    bool isSelectionMode,
    VoidCallback onDone,
  ) {
    if (!isSelectionMode) return const SizedBox.shrink();

    return Container(
      padding: const EdgeInsets.all(16.0),
      decoration: BoxDecoration(
        color: Theme.of(context).colorScheme.primaryContainer,
        borderRadius: const BorderRadius.only(
          bottomLeft: Radius.circular(12.0),
          bottomRight: Radius.circular(12.0),
        ),
      ),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          TextButton.icon(
            onPressed: () {
              setState(() {
                selectedTasks.clear();
              });
            },
            icon: const Icon(Icons.clear),
            label: const Text('Clear Selection'),
          ),
          ElevatedButton.icon(
            onPressed: onDone,
            icon: const Icon(Icons.task_alt),
            label: Text('Complete (${selectedItems.length})'),
            style: ElevatedButton.styleFrom(
              backgroundColor: Theme.of(context).colorScheme.primary,
              foregroundColor: Theme.of(context).colorScheme.onPrimary,
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildCustomDragHandle(
    BuildContext context,
    bool isSelected,
    bool isDragging,
  ) {
    return Padding(
      padding: const EdgeInsets.all(4.0),
      child: Icon(
        Icons.drag_indicator,
        color: isDragging
            ? Theme.of(context).colorScheme.primary
            : Theme.of(context)
                .colorScheme
                .onSurfaceVariant
                .withValues(alpha: 0.7),
        size: 24,
      ),
    );
  }

  // Helper methods
  Widget _getPriorityIcon(TaskPriority priority) {
    IconData iconData;
    Color color;

    switch (priority) {
      case TaskPriority.low:
        iconData = Icons.arrow_downward;
        color = Colors.green;
        break;
      case TaskPriority.medium:
        iconData = Icons.remove;
        color = Colors.orange;
        break;
      case TaskPriority.high:
        iconData = Icons.arrow_upward;
        color = Colors.red;
        break;
    }

    return Container(
      padding: const EdgeInsets.all(4.0),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.1),
        borderRadius: BorderRadius.circular(4.0),
      ),
      child: Icon(
        iconData,
        color: color,
        size: 16,
      ),
    );
  }

  String _formatDate(DateTime date) {
    final now = DateTime.now();
    final difference = date.difference(now).inDays;

    if (difference == 0) {
      return 'Today';
    } else if (difference == 1) {
      return 'Tomorrow';
    } else if (difference > 1 && difference < 7) {
      return '$difference days';
    } else {
      return '${date.day}/${date.month}/${date.year}';
    }
  }

  bool _isOverdue(DateTime date) {
    final now = DateTime.now();
    return date.isBefore(now);
  }

  void _addNewTask() {
    final newIndex = tasks.length;
    setState(() {
      tasks.add(
        TaskItem(
          id: 'task_${DateTime.now().millisecondsSinceEpoch}',
          title: 'New Task ${newIndex + 1}',
          description: 'Description for new task ${newIndex + 1}',
          priority: TaskPriority.medium,
          dueDate: DateTime.now().add(const Duration(days: 3)),
          isCompleted: false,
        ),
      );
    });
  }

  void _markTasksAsCompleted(List<TaskItem> tasksToComplete) {
    setState(() {
      for (final task in tasksToComplete) {
        final index = tasks.indexWhere((t) => t.id == task.id);
        if (index != -1) {
          tasks[index] = tasks[index].copyWith(isCompleted: true);
        }
      }
      selectedTasks = [];
    });

    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('Marked ${tasksToComplete.length} tasks as completed'),
        duration: const Duration(seconds: 2),
      ),
    );
  }
}

// Task data model
class TaskItem {
  final String id;
  final String title;
  final String description;
  final TaskPriority priority;
  final DateTime dueDate;
  final bool isCompleted;

  const TaskItem({
    required this.id,
    required this.title,
    required this.description,
    required this.priority,
    required this.dueDate,
    required this.isCompleted,
  });

  TaskItem copyWith({
    String? id,
    String? title,
    String? description,
    TaskPriority? priority,
    DateTime? dueDate,
    bool? isCompleted,
  }) {
    return TaskItem(
      id: id ?? this.id,
      title: title ?? this.title,
      description: description ?? this.description,
      priority: priority ?? this.priority,
      dueDate: dueDate ?? this.dueDate,
      isCompleted: isCompleted ?? this.isCompleted,
    );
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is TaskItem && other.id == id;
  }

  @override
  int get hashCode => id.hashCode;
}

// Task priority enum
enum TaskPriority {
  low,
  medium,
  high,
}

// Pagination Example Screen
class PaginationExampleScreen extends StatefulWidget {
  const PaginationExampleScreen({super.key});

  @override
  State<PaginationExampleScreen> createState() =>
      _PaginationExampleScreenState();
}

class _PaginationExampleScreenState extends State<PaginationExampleScreen> {
  // Sample data with pagination
  List<ItemData> items = [];

  // Create a global key to access the list state
  final listKey = GlobalKey<ReorderableMultiDragListState<ItemData>>();

  // Pagination settings
  final int pageSize = 15;
  int totalItems = 200; // simulate a large dataset
  bool isLoading = false;

  @override
  void initState() {
    super.initState();
    // Load initial data
    _loadInitialData();
  }

  // Simulates loading initial data
  Future<void> _loadInitialData() async {
    setState(() {
      isLoading = true;
    });

    // Simulate network delay
    await Future.delayed(const Duration(seconds: 1));

    setState(() {
      // Generate initial items
      items = List.generate(
        pageSize,
        (index) => ItemData(
          id: 'item_$index',
          title: 'Item ${index + 1}',
          subtitle: 'Description for item ${index + 1}',
          color: Colors.primaries[index % Colors.primaries.length],
        ),
      );
      isLoading = false;
    });
  }

  // Load more data when scrolling
  Future<void> _loadMoreData(int page, int pageSize) async {
    // Skip if we've loaded all items
    if (items.length >= totalItems) return;

    // Simulate network delay
    await Future.delayed(const Duration(milliseconds: 800));

    setState(() {
      // Calculate the start index for the next page
      final startIndex = items.length;
      // Determine how many more items to add (handle reaching the end)
      final itemsToAdd = (startIndex + pageSize > totalItems)
          ? totalItems - startIndex
          : pageSize;

      // Add new items
      items.addAll(
        List.generate(
          itemsToAdd,
          (index) {
            final actualIndex = startIndex + index;
            return ItemData(
              id: 'item_$actualIndex',
              title: 'Item ${actualIndex + 1}',
              subtitle: 'Description for item ${actualIndex + 1}',
              color: Colors.primaries[actualIndex % Colors.primaries.length],
            );
          },
        ),
      );
    });
  }

  // Refresh all data
  Future<void> _refreshData() async {
    setState(() {
      isLoading = true;
    });

    // Simulate network delay
    await Future.delayed(const Duration(seconds: 1));

    setState(() {
      // Generate fresh items
      items = List.generate(
        pageSize,
        (index) => ItemData(
          id: 'item_$index',
          title: 'Item ${index + 1} (Refreshed)',
          subtitle: 'Updated description for item ${index + 1}',
          color: Colors.primaries[index % Colors.primaries.length],
        ),
      );
      isLoading = false;
    });

    // Refresh the list widget
    listKey.currentState?.refreshItems(resetPagination: true);

    // Show success message
    // ignore: use_build_context_synchronously
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Data refreshed successfully'),
        duration: Duration(seconds: 2),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Pagination Example'),
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: _refreshData,
            tooltip: 'Refresh data',
          ),
        ],
      ),
      body: isLoading
          ? const Center(child: CircularProgressIndicator())
          : Column(
              children: [
                Padding(
                  padding: const EdgeInsets.all(16),
                  child: Text(
                    'Showing ${items.length} of $totalItems items',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                ),
                Expanded(
                  child: ReorderableMultiDragList<ItemData>(
                    listKey: listKey,
                    items: items,
                    pageSize: pageSize,
                    onPageRequest: _loadMoreData,
                    loadingWidgetBuilder: (context) {
                      return Column(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          const CircularProgressIndicator(),
                          const SizedBox(height: 16),
                          Text(
                            'Loading more items...',
                            style: TextStyle(
                              fontStyle: FontStyle.italic,
                              color: Theme.of(context).colorScheme.primary,
                            ),
                          ),
                        ],
                      );
                    },
                    itemBuilder:
                        (context, item, index, isSelected, isDragging) {
                      return ListTile(
                        title: Text(item.title),
                        subtitle: Text(item.subtitle),
                        leading: CircleAvatar(
                          backgroundColor: item.color,
                          child: Text('${index + 1}'),
                        ),
                      );
                    },
                    onReorder: (newIndex, movedItems, reorderedItems) {
                      setState(() {
                        items = reorderedItems;
                      });
                    },
                    headerWidget: Container(
                      padding: const EdgeInsets.symmetric(
                          horizontal: 16, vertical: 8),
                      color:
                          Theme.of(context).colorScheme.surfaceContainerHighest,
                      child: const Text(
                        'Scroll down to load more items',
                        style: TextStyle(fontStyle: FontStyle.italic),
                      ),
                    ),
                    footerWidget: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Text(
                        'End of list - ${items.length} items loaded',
                        style: TextStyle(
                          color: Theme.of(context).colorScheme.secondary,
                          fontStyle: FontStyle.italic,
                        ),
                      ),
                    ),
                    theme: ReorderableMultiDragTheme(
                      draggedItemBorderColor:
                          Theme.of(context).colorScheme.primary,
                    ),
                  ),
                ),
              ],
            ),
      floatingActionButton: FloatingActionButton(
        onPressed: _refreshData,
        tooltip: 'Refresh',
        child: const Icon(Icons.refresh),
      ),
    );
  }
}
8
likes
160
points
347
downloads

Publisher

verified publishercodesters-inc.com

Weekly Downloads

A Flutter package for multi-selection and animated reordering of items.

Homepage
Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on multi_reorderable