selectable_draggable_listbox 0.0.3
selectable_draggable_listbox: ^0.0.3 copied to clipboard
Listbox with multiselect, drag & drop between lists, and reorder built in.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:selectable_draggable_listbox/selectable_draggable_listbox.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Selectable Draggable Listbox Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Selectable Draggable Listbox Demo'),
);
}
}
class GroceryItem {
final String name;
DateTime? lastBought;
String get lastBoughtShortDtTm => lastBought == null
? ''
: DateFormat('MM/dd/yyyy kk:mm').format(lastBought!);
GroceryItem({
required this.name,
this.lastBought,
});
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: const Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
// See this widget for: Multi-select, Reorder, and Drag From
GroceryListWidget(),
// See this widget for: Single-select, Customized Template, and Drag To
RecentListWidget(),
],
),
),
);
}
}
class GroceryListWidget extends StatefulWidget {
const GroceryListWidget({
super.key,
});
@override
State<GroceryListWidget> createState() => _GroceryListWidgetState();
}
class _GroceryListWidgetState extends State<GroceryListWidget> {
final _groceryList = [
GroceryItem(name: 'Apples'),
GroceryItem(name: 'Bananas'),
GroceryItem(name: 'Milk'),
GroceryItem(name: 'Cheese'),
GroceryItem(name: 'Bread'),
].forListbox().toList();
@override
Widget build(BuildContext context) {
/// Make a simple listbox item
/// This demonstrates using the index to show a number prefix
/// Slightly different for the "drag template" that follows your mouse pointer - removes the number prefix
Widget makeItemTemplate(
int index,
ListItem<GroceryItem> item,
void Function(ListItem<GroceryItem>)? onSelect,
bool isDragging,
) {
return SimpleListboxItem(
key: Key('$index'),
item: item,
label: isDragging ? item.data.name : '${index + 1}. ${item.data.name}',
onSelect: onSelect,
isDragging: isDragging,
);
}
void onSelect(Iterable<ListItem<GroceryItem>> itemsSelected) {
debugPrint(
'Selected: ${itemsSelected.map((e) => e.data.name).join(',')}');
setState(() {
for (var item in _groceryList) {
item.isSelected = itemsSelected.contains(item);
}
});
}
void onReorder(int oldIndex, int newIndex) {
debugPrint('Moving item from $oldIndex to $newIndex');
setState(() {
// Note: this is an extension provided by the package
_groceryList.move(oldIndex, newIndex);
});
}
return Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'Grocery List',
style: Theme.of(context).textTheme.displaySmall,
),
const Text('Features: Multi-select, Reorder, Drag From'),
Expanded(
child: Builder(
builder: (context) {
return Listbox(
// Key is only needed if you want to identify which list you're interacting with in debug:
key: const Key('GroceryList'),
items: _groceryList,
onSelect: onSelect,
onReorder: onReorder,
itemTemplate: (context, index, item, onSelect) =>
makeItemTemplate(index, item, onSelect, false),
dragTemplate: (context, index, item) =>
makeItemTemplate(index, item, null, true),
// Show info in console about how the list is being interacted with:
enableDebug: true,
);
},
),
),
],
),
),
);
}
}
class RecentListWidget extends StatefulWidget {
const RecentListWidget({
super.key,
});
@override
State<RecentListWidget> createState() => _RecentListWidgetState();
}
class _RecentListWidgetState extends State<RecentListWidget> {
final _recentList = [
GroceryItem(
name: 'Apples',
lastBought: DateTime(2024, 3, 3, 13, 22, 33),
),
GroceryItem(
name: 'Bread',
lastBought: DateTime(2024, 3, 4, 10, 14, 12),
)
].forListbox().toList();
@override
Widget build(BuildContext context) {
/// Make a more complex listbox item
/// This demonstrates using the childTemplate to show a row with a badge
/// indicating the date the item was dragged to this list
Widget makeItemTemplate(
int index,
ListItem<GroceryItem> item,
String label,
void Function(ListItem<GroceryItem>)? onSelect,
bool isDragPlaceholder,
) {
return TemplatedListboxItem(
key: Key('$index'),
item: item,
childTemplate: (context, item) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(label),
if (!isDragPlaceholder)
Badge(
label: Text(item.data.lastBoughtShortDtTm),
),
],
),
);
},
onSelect: onSelect,
);
}
void onSelect(Iterable<ListItem<GroceryItem>> itemsSelected) {
debugPrint(
'Selected: ${itemsSelected.map((e) => e.data.name).join(',')}');
setState(() {
for (var item in _recentList) {
item.isSelected = itemsSelected.contains(item);
}
});
}
void onDrop(Iterable<ListItem<GroceryItem>> itemsDropped, int index) {
debugPrint(
'Dropped items ${itemsDropped.map((e) => e.data.name).join(',')} into index $index');
// It is important to copy the items not just accept them
// (otherwise they'd have same reference and selected state would cross).
// Also in this example, we'll avoid adding duplicates.
final existingNames = _recentList.map((i) => i.data.name);
final itemsToInsert = itemsDropped
.where((i) => !existingNames.contains(i.data.name))
.map((i) => ListItem(
GroceryItem(name: i.data.name, lastBought: DateTime.now())))
.toList();
_recentList.insertAll(index, itemsToInsert);
}
return Flexible(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text(
'Recently Bought',
style: Theme.of(context).textTheme.displaySmall,
),
const Text('Features: Single-select, Customized Template, Drag To'),
Expanded(
child: Builder(
builder: (context) {
return Listbox(
// Key is only needed if you want to identify which list you're interacting with in debug:
key: const Key('RecentList'),
items: _recentList,
onSelect: onSelect,
onDrop: onDrop,
itemTemplate: (context, index, item, onSelect) =>
makeItemTemplate(
index, item, item.data.name, onSelect, false),
dropPlaceholderTemplate:
(context, index, item, itemsToBeDropped) {
var itemsLength = itemsToBeDropped.length;
// Here we're going to adjust the label that shows in the new list's placeholder,
// depending on whether we are dragging 1 item (just show name) or more (count of new items)
var label = itemsLength > 1
? '$itemsLength new items...'
: itemsLength > 0
? itemsToBeDropped.first.data.name
: '';
return makeItemTemplate(index, item, label, null, true);
},
// Can just turn off multiselect if not needed:
disableMultiSelect: true,
// Show info in console about how the list is being interacted with:
enableDebug: true,
);
},
),
),
],
),
),
);
}
}