下面是我的工作代码的最小复制,其中创建了一个动态列表。该列表在开始时已使用单个元素进行初始化。用户可以在按下按钮时添加更多项目,或者用户可以通过从末尾滑动到开始来关闭该项目。
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),
],
);
}
}
问题: 添加或取消项目时,文本字段/下拉菜单中的更改将丢失。
寻找: 您能否建议一种方法,以便列表项顶部容器中显示的项目
index
也更新为新值,但保持用户输入完整。
每次刷新状态时,都会重新创建 InvoiceItemInputs。当然,您在内部定义的 TextEditingController 也会被重新创建。这似乎是问题的根源。
记住:setState方法会触发关联StatefulWidget的build方法。
在这种情况下,如果你想防止数据丢失,你应该在外部定义控制器。
下面,我将提供一个建议:
class _MyAppState extends State<MyApp> {
late List<InvoiceItemInput> itemsList;
List<TextEditingController> itemControllers = [];
@override
void initState() {
super.initState();
final controller = TextEditingController();
itemsList = List<InvoiceItemInput>.from([
InvoiceItemInput(
parentWidth: 400.0,
index: 1,
controller: controller,
key: ValueKey('1' + DateTime.now().toString()),
)
]);
itemControllers.add(controller);
}
@override
void dispose() {
for (var controller in itemControllers) {
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) {
setState(() {
itemsList.removeAt(index);
itemControllers.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;
var controller = TextEditingController();
itemControllers.add(controller);
itemsList.add(InvoiceItemInput(
parentWidth: 400.0,
index: index + 1,
controller: controller,
key: ValueKey(
index.toString() + DateTime.now().toString()),
));
});
}),
),
),
],
),
),
),
);
}
}
class InvoiceItemInput extends StatefulWidget {
const InvoiceItemInput(
{super.key,
required this.parentWidth,
required this.index,
required this.controller});
final double parentWidth;
final int index;
final TextEditingController controller;
@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.controller,
decoration: InputDecoration(
labelText: 'Item Name *',
hintText: 'Enter Item Name',
),
),
),
],
),
SizedBox(height: 15.0),
],
);
}
}
基于@Altay的响应,我的答案也解决了更新项目索引的问题。我仍然有一个问题在下面的代码中作为注释提到。我们是否需要从列表中删除已删除的 TextControllers() ?怎么办?
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),
],
);
}
}