You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter动态表单中DropdownButton选中值控制与移除问题

动态Flutter表单的DropdownButton状态同步问题
  • 开发动态添加分组字段的Flutter表单,初始仅显示添加按钮,用户可添加任意多组表单,每组包含两个TextFormField和一个DropdownButton
  • 现有问题:添加4组表单填写内容后,移除第二组时,ListView会错误删除最后一项,但列表数据已在选中索引处正确移除;同时DropdownButton的选中值无法与数据同步,移除项后状态混乱
  • 已完成:TextFormField的控制器列表管理及销毁逻辑

现有代码

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Purchased(),
    );
  }
}

class Purchased extends StatefulWidget {
  const Purchased({Key? key}) : super(key: key);

  @override
  State<Purchased> createState() => _PurchasedState();
}

class _PurchasedState extends State<Purchased> {
  List<UserInfo> list = [];
  List<TextEditingController> textControllerList = [];
  List<TextEditingController> textControllerList1 = [];
  

  @override
  void dispose() {
    textControllerList.forEach((element) {
      element.dispose();
    });
    textControllerList1.forEach((element) {
      element.dispose();
    });
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          list.add(UserInfo());
          setState(() {});
        },
        child: const Icon(Icons.add),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: ((context, index) {
                  return Column(
                    children: [
                      const Text('phone'),
                      Text(list[index].phone),
                      const Text('email'),
                      Text(list[index].email),
                      Text('category'),
                      Text(list[index].category)
                    ],
                  );
                })),
          ),
          Expanded(
            child: ListView.builder(
                shrinkWrap: true,
                itemCount: list.length,
                itemBuilder: ((context, index) {
                  TextEditingController controller = TextEditingController();
                  TextEditingController controller1 = TextEditingController();
                  textControllerList.add(controller);
                  textControllerList1.add(controller1);
                  return MyForm(
                      textEditingController: textControllerList[index],
                    textEditingController1: textControllerList1[index],
                      key: ValueKey(index),
                      initInfo: list[index],
                      onChangePhone: (phoneVal) {
                        if (phoneVal != null) {
                          setState(() {
                            list[index].setPhone(phoneVal);
                          });
                        }
                      },
                    onChangeEmail: (emailVal) {
                        if (emailVal != null) {
                          list[index].setEmail(emailVal);
                          setState(() {});
                        }
                      },
                    onChangeCategory: (categoryVal) {
                        if (categoryVal != null) {
                          list[index].setCategory(categoryVal);
                          setState(() {});
                        }
                      },
                      onremove: () {
                        list.removeAt(index);
                        textControllerList.removeAt(index);
                        textControllerList1.removeAt(index);
                        setState(() {});
                      });
                })),
          )
        ],
      ),
    );
  }
}

class MyForm extends StatefulWidget {
  final UserInfo initInfo;
  final Function(String?) onChangePhone;
  final Function(String?) onChangeEmail;
  final Function(String?) onChangeCategory;
  final TextEditingController textEditingController;
  final TextEditingController textEditingController1;

  final VoidCallback? onremove;
  const MyForm({
    super.key,
    required this.initInfo,
    required this.onChangePhone,
    required this.onChangeEmail,
    required this.onChangeCategory,
    required this.onremove,
    required this.textEditingController,
    required this.textEditingController1,
  });

  @override
  State<MyForm> createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  
  List<UserInfo> list = <UserInfo>[];
  
  final List<String> category = [
    'Manager',
    'Reception',
    'Sales',
    'Service',
  ];
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      child: Column(
        children: [
          IconButton(
              onPressed: widget.onremove,
              icon: const Icon(
                Icons.remove,
              )),
          TextFormField(
            controller: widget.textEditingController,
            onChanged: widget.onChangePhone,
          ),
          TextFormField(
            controller: widget.textEditingController1,
            onChanged: widget.onChangeEmail,
          ),
          DropdownButton(
              isExpanded: true,
              hint: const Text(
                'Select Category',
                style: TextStyle(fontSize: 14),
              ),
              icon: const Icon(
                Icons.arrow_drop_down,
                color: Colors.black45,
              ),
              iconSize: 30,
              items: category
                  .map((item) => DropdownMenuItem<String>(
                        value: item,
                        child: Text(
                          item,
                          style: const TextStyle(
                            fontSize: 14,
                          ),
                        ),
                      ))
                  .toList(),
              onChanged: widget.onChangeCategory)
        ],
      ),
    );
  }
}

class UserInfo {
  String _phone = '';
  String _email = '';
  String _category = '';

  String get phone => _phone;
  String get email => _email;
  String get category => _category;

  void setPhone(String phone) {
    _phone = phone;
  }
  void setEmail(String email) {
    _email = email;
  }
  void setCategory(String category) {
    _category = category;
  }
}

解决方案

修改后完整代码

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Purchased(),
    );
  }
}

class Purchased extends StatefulWidget {
  const Purchased({Key? key}) : super(key: key);

  @override
  State<Purchased> createState() => _PurchasedState();
}

class _PurchasedState extends State<Purchased> {
  List<UserInfo> list = [];
  List<TextEditingController> phoneControllers = [];
  List<TextEditingController> emailControllers = [];

  @override
  void dispose() {
    phoneControllers.forEach((controller) => controller.dispose());
    emailControllers.forEach((controller) => controller.dispose());
    super.dispose();
  }

  void _addNewForm() {
    setState(() {
      list.add(UserInfo());
      phoneControllers.add(TextEditingController());
      emailControllers.add(TextEditingController());
    });
  }

  void _removeForm(int index) {
    setState(() {
      list.removeAt(index);
      phoneControllers.removeAt(index).dispose();
      emailControllers.removeAt(index).dispose();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(
        onPressed: _addNewForm,
        child: const Icon(Icons.add),
      ),
      body: Column(
        children: [
          Expanded(
            child: ListView.builder(
              shrinkWrap: true,
              itemCount: list.length,
              itemBuilder: (context, index) {
                final info = list[index];
                return Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Text('Phone:'),
                      Text(info.phone),
                      const SizedBox(height: 4),
                      const Text('Email:'),
                      Text(info.email),
                      const SizedBox(height: 4),
                      const Text('Category:'),
                      Text(info.category.isEmpty ? '未选择' : info.category),
                      const Divider(),
                    ],
                  ),
                );
              },
            ),
          ),
          Expanded(
            child: ListView.builder(
              shrinkWrap: true,
              itemCount: list.length,
              itemBuilder: (context, index) {
                return MyForm(
                  key: ObjectKey(list[index]),
                  initInfo: list[index],
                  phoneController: phoneControllers[index],
                  emailController: emailControllers[index],
                  onPhoneChanged: (value) {
                    if (value != null) {
                      list[index].setPhone(value);
                    }
                  },
                  onEmailChanged: (value) {
                    if (value != null) {
                      list[index].setEmail(value);
                    }
                  },
                  onCategoryChanged: (value) {
                    if (value != null) {
                      setState(() {
                        list[index].setCategory(value);
                      });
                    }
                  },
                  onRemove: () => _removeForm(index),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

class MyForm extends StatelessWidget {
  final UserInfo initInfo;
  final TextEditingController phoneController;
  final TextEditingController emailController;
  final Function(String?) onPhoneChanged;
  final Function(String?) onEmailChanged;
  final Function(String?) onCategoryChanged;
  final VoidCallback onRemove;

  final List<String> categories = [
    'Manager',
    'Reception',
    'Sales',
    'Service',
  ];

  const MyForm({
    super.key,
    required this.initInfo,
    required this.phoneController,
    required this.emailController,
    required this.onPhoneChanged,
    required this.onEmailChanged,
    required this.onCategoryChanged,
    required this.onRemove,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(12),
      margin: const EdgeInsets.only(bottom: 16),
      decoration: BoxDecoration(
        border: Border.all(color: Colors.grey.shade300),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        children: [
          Align(
            alignment: Alignment.centerRight,
            child: IconButton(
              onPressed: onRemove,
              icon: const Icon(Icons.remove_circle, color: Colors.red),
            ),
          ),
          TextFormField(
            controller: phoneController,
            onChanged: onPhoneChanged,
            decoration: const InputDecoration(
              labelText: 'Phone',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 12),
          TextFormField(
            controller: emailController,
            onChanged: onEmailChanged,
            decoration: const InputDecoration(
              labelText: 'Email',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 12),
          DropdownButtonFormField<String>(
            isExpanded: true,
            value: initInfo.category.isEmpty ? null : initInfo.category,
            hint: const Text('Select Category'),
            icon: const Icon(Icons.arrow_drop_down, color: Colors.black45),
            decoration: const InputDecoration(
              border: OutlineInputBorder(),
            ),
            items: categories.map((category) {
              return DropdownMenuItem(
                value: category,
                child: Text(category),
              );
            }).toList(),
            onChanged: onCategoryChanged,
          ),
        ],
      ),
    );
  }
}

class UserInfo {
  String _phone = '';
  String _email = '';
  String _category = '';

  String get phone => _phone;
  String get email => _email;
  String get category => _category;

  void setPhone(String phone) => _phone = phone;
  void setEmail(String email) => _email = email;
  void setCategory(String category) => _category = category;
}

核心修改说明

  1. 修复ListView项删除错误

    • 将ValueKey(index)替换为ObjectKey(list[index]),用数据对象作为唯一标识,避免索引变化导致的Widget复用错误
    • 提取添加/删除表单的逻辑为单独函数,避免build方法内重复创建控制器
  2. DropdownButton状态同步

    • 使用DropdownButtonFormField替代原生DropdownButton,贴合表单场景
    • 绑定value属性到initInfo.category,确保下拉框显示当前选中值
    • 在onChanged回调中调用setState,保证数据更新后UI同步
  3. 优化控制器管理

    • 添加新表单时同步创建对应控制器,避免在itemBuilder中重复添加
    • 删除表单时立即销毁对应控制器,防止内存泄漏

内容的提问来源于stack exchange,提问作者Mohammed Bekele

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 04:45:33