创建包含 TextField 的动态列表时,用户输入和索引在添加或取消列表项时会丢失(Flutter)

问题描述 投票:0回答:2

下面是我的工作代码的最小复制,其中创建了一个动态列表。该列表在开始时已使用单个元素进行初始化。用户可以在按下按钮时添加更多项目,或者用户可以通过从末尾滑动到开始来关闭该项目。

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
也更新为新值,但保持用户输入完整。

flutter listview
2个回答
1
投票

每次刷新状态时,都会重新创建 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),
      ],
    );
  }
}

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),
      ],
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.