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.
2
Answers
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?
Each time the state is refreshed, the InvoiceItemInputs are recreated. Naturally, the TextEditingControllers you defined inside are also recreated. This seems to be the source of the problem.
Remember: the setState method triggers the build method of the associated StatefulWidget.
In this case, if you want to prevent your data from being lost, you should define the controllers outside.
Below, I will provide a suggestion: