Below is my minimal reproduction of my working code, where a dynamic list is created. The list has been initialised with a single element at the start. User can add more items on pressing a button or user can dismiss the item by swiping from end to start.
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late List<InvoiceItemInput> itemsList;
@override
void initState() {
itemsList = List<InvoiceItemInput>.from([
InvoiceItemInput(
parentWidth: 400.0,
index: 1,
key: ValueKey('1' + DateTime.now().toString()),
)
]);
super.initState();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) => Dismissible(
key: ValueKey(index.toString() + DateTime.now().toString()),
child: itemsList[index],
background: Container(color: Colors.red),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
if (direction == DismissDirection.endToStart) {
setState(() {
itemsList.removeAt(index);
});
}
}),
itemCount: itemsList.length,
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20.0),
child: Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
child: Text('ADD'),
onPressed: () {
setState(() {
final int index = itemsList.length;
itemsList.add(InvoiceItemInput(
parentWidth: 400.0,
index: index + 1,
key: ValueKey(
index.toString() + DateTime.now().toString()),
));
});
}),
),
),
],
),
),
),
);
}
}
class InvoiceItemInput extends StatefulWidget {
const InvoiceItemInput(
{super.key, required this.parentWidth, required this.index});
final double parentWidth;
final int index;
@override
State<InvoiceItemInput> createState() => _InvoiceItemInputState();
}
class _InvoiceItemInputState extends State<InvoiceItemInput> {
late TextEditingController? itemController;
final double horizontalSpacing = 15.0;
bool showDeleteButton = false;
@override
void initState() {
itemController = TextEditingController();
super.initState();
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12.0),
Container(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 7.0),
child: Text('Item ${widget.index}',
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(color: Colors.white)),
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(7.0)),
),
SizedBox(height: 7.0),
Wrap(
spacing: this.horizontalSpacing,
runSpacing: 25.0,
children: [
SizedBox(
width: (widget.parentWidth - horizontalSpacing) / 2 < 200.0
? (widget.parentWidth - horizontalSpacing) / 2
: 200.0,
child: DropdownMenu(
controller: itemController,
label: Text(
'Item Name *',
style: const TextStyle(
fontFamily: 'Raleway',
fontSize: 14.0,
color: Colors.black87,
fontWeight: FontWeight.w500),
),
hintText: 'Enter Item Name',
requestFocusOnTap: true,
enableFilter: true,
expandedInsets: EdgeInsets.zero,
textStyle: Theme.of(context).textTheme.bodySmall,
menuStyle: MenuStyle(
backgroundColor: WidgetStateProperty.all(Colors.lightBlue),
),
dropdownMenuEntries: [
DropdownMenuEntry(
value: 'Pen',
label: 'Pen',
style: MenuItemButton.styleFrom(
foregroundColor: Colors.white,
textStyle: Theme.of(context).textTheme.bodySmall,
),
),
DropdownMenuEntry(
value: 'Pencil',
label: 'Pencil',
style: MenuItemButton.styleFrom(
foregroundColor: Colors.white,
textStyle: Theme.of(context).textTheme.bodySmall,
),
)
],
),
),
],
),
SizedBox(height: 15.0),
],
);
}
}
Problem: When an item is added or dismissed, the changes are lost in the TextField / DropdownMenu.
Looking for:
Could you please suggest a way out so that the Item index
shown in a Container at top of list item also updates to new value but keeping the user input intact.
Building upon @Altay 's response, my answer solves updating index of an item problem too. I still have a question mentioned as a comment in the code below. Do we need to dispose removed TextControllers() from the list? how?
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
List<ValueKey> itemKeys = [];
List<Map<String, dynamic>> itemsControllers = [];
late List<InvoiceItemInput> itemsList;
@override
void initState() {
super.initState();
itemKeys.add(ValueKey('1' + DateTime.now().toString()));
itemsControllers.add({
'key': itemKeys[0],
'itemControllers':
Set<TextEditingController>.unmodifiable([TextEditingController()]) // Using Set here in case we have multiple text controllers here
});
itemsList = List<InvoiceItemInput>.from([
InvoiceItemInput(
parentWidth: 400.0,
index: 1,
key: itemKeys[0],
controllers: itemsControllers[0]['itemControllers']!,
)
]);
}
@override
void dispose() {
for (int k = 0; k < itemsControllers.length; k++) {
itemsControllers[k]['itemControllers']
.forEach((controller) => controller.dispose());
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
ListView.builder(
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) => Dismissible(
key: ValueKey(index.toString() + DateTime.now().toString()),
child: itemsList[index],
background: Container(color: Colors.red),
direction: DismissDirection.endToStart,
onDismissed: (direction) {
if (direction == DismissDirection.endToStart) {
final removeKey = itemKeys.elementAt(index);
itemKeys.removeAt(index);
itemsList.removeWhere(
(invoiceItem) => invoiceItem.key == removeKey);
itemsControllers.removeWhere(
(itemControl) => itemControl['key'] == removeKey); // Question: Do we need to dispose these controllers removed, if so how can we do ?
itemsList.indexed
.forEach(((int, InvoiceItemInput) item) {
if (item.$2.index > item.$1 + 1) {
final replacementItem = InvoiceItemInput(
parentWidth: 400.0,
index: item.$1 + 1,
key: item.$2.key,
controllers: item.$2.controllers,
);
itemsList.removeAt(item.$1);
itemsList.insert(item.$1, replacementItem);
}
});
setState(() {});
}
}),
itemCount: itemsList.length,
),
Padding(
padding: EdgeInsets.symmetric(vertical: 20.0),
child: Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
child: Text('ADD'),
onPressed: () {
final int index = itemsList.length;
itemKeys.add(ValueKey(
index.toString() + DateTime.now().toString()));
itemsControllers.add({
'key': itemKeys[index],
'itemControllers':
Set<TextEditingController>.unmodifiable(
[TextEditingController()])
});
setState(() {
itemsList.add(InvoiceItemInput(
parentWidth: 400.0,
index: index + 1,
key: itemKeys[index],
controllers: itemsControllers[index]
['itemControllers'],
));
});
}),
),
),
],
),
),
),
);
}
}
class InvoiceItemInput extends StatefulWidget {
const InvoiceItemInput(
{Key? key,
required this.parentWidth,
required this.index,
required this.controllers})
: super(key: key);
final double parentWidth;
final int index;
final Set<TextEditingController> controllers;
@override
State<InvoiceItemInput> createState() => _InvoiceItemInputState();
}
class _InvoiceItemInputState extends State<InvoiceItemInput> {
final double horizontalSpacing = 15.0;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 12.0),
Container(
padding: EdgeInsets.symmetric(vertical: 4.0, horizontal: 7.0),
child: Text('Item ${widget.index}',
style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(color: Colors.white)),
decoration: BoxDecoration(
color: Colors.lightBlue,
borderRadius: BorderRadius.circular(7.0)),
),
SizedBox(height: 7.0),
Wrap(
spacing: this.horizontalSpacing,
runSpacing: 25.0,
children: [
SizedBox(
width: (widget.parentWidth - horizontalSpacing) / 2 < 200.0
? (widget.parentWidth - horizontalSpacing) / 2
: 200.0,
child: TextField(
controller: widget.controllers.elementAt(0),
decoration: InputDecoration(
labelText: 'Item Name *',
hintText: 'Enter Item Name',
),
),
),
],
),
SizedBox(height: 15.0),
],
);
}
}