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

Flutter中ModalBottomSheet内DropdownButton选中值不更新UI问题

Flutter ModalBottomSheet内DropdownButton选中值UI不更新问题

我是Flutter新手,做了个从Cloud Firestore读取person集合文档的项目。点击界面的+按钮会弹出ModalBottomSheet用来新增person文档,但弹窗里的department下拉选单选中新值后,日志能打出选中的新值,UI却没同步更新。我猜是context的问题,但一直解决不了。

代码示例

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';

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

  @override
  _PersonState createState() => _PersonState();
}

class _PersonState extends State<Person> {
  final TextEditingController _nameController = TextEditingController();
  final CollectionReference _person = FirebaseFirestore.instance.collection('person');
  final CollectionReference _department = FirebaseFirestore.instance.collection('department');
  String? _usersDeptName;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: SafeArea(
          child: Column(
            children: [
              Expanded(
                child: StreamBuilder(
                  stream: _person.snapshots(),
                  builder: (context, AsyncSnapshot<QuerySnapshot> streamSnapshot) {
                    if (streamSnapshot.hasData) {
                      return ListView.builder(
                        itemCount: streamSnapshot.data!.docs.length,
                        itemBuilder: (context, index) {
                          final DocumentSnapshot documentSnapshot = streamSnapshot.data!.docs[index];
                          return Card(
                            margin: const EdgeInsets.all(10),
                            child: ListTile(
                              title: Text(documentSnapshot['personName']),
                              subtitle: Text(documentSnapshot['departmentName'] ?? '?'),
                            ),
                          );
                        },
                      );
                    }
                    return const Center(
                      child: CircularProgressIndicator(),
                    );
                  },
                ),
              ),
            ],
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => {_create()},
          child: const Icon(Icons.add),
        ),
        floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat);
  }

  Future<void> _create() async {
    _usersDeptName = null;
    await showModalBottomSheet(
      isScrollControlled: true,
      context: context,
      builder: (BuildContext ctx) {
        return Padding(
          padding: EdgeInsets.only(top: 20, left: 20, right: 20, bottom: MediaQuery.of(context).viewInsets.bottom + 20),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              TextField(controller: _nameController, decoration: InputDecoration(labelText: 'person_name'.tr())),
              const SizedBox(height: 10),
              StreamBuilder<QuerySnapshot>(
                stream: _department.snapshots(),
                builder: (context, snapshot) {
                  if (!snapshot.hasData) {
                    return Text("loading").tr();
                  } else {
                    List<DropdownMenuItem> departments = [];
                    int? howManyRecords = snapshot.data?.size;
                    for (int i = 0; i < howManyRecords!; i++) {
                      DocumentSnapshot snap = snapshot.data?.docs[i] as DocumentSnapshot<Object?>;
                      departments.add(DropdownMenuItem(child: Text(snap.get('departmentName')), value: snap.get('departmentName')));
                    }
                    return Row(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: <Widget>[
                        Expanded(
                          child: DropdownButton(
                            value: _usersDeptName,
                            items: departments,
                            onChanged: (newValue) {
                              setState(() {
                                _usersDeptName = newValue.toString();
                                print('$_usersDeptName is selected');
                              });
                            },
                            isExpanded: true,
                          ),
                        ),
                      ],
                    );
                  }
                },
              ),
              ElevatedButton(
                child: const Text('save').tr(),
                onPressed: () async {
                  final String name = _nameController.text;
                  if (name != null) {
                    await _person.add({"personName": name, 'departmentName': _usersDeptName});
                    _nameController.text = '';
                    Navigator.of(context).pop();
                  }
                },
              )
            ],
          ),
        );
      },
    );
  }
}

相关截图

  • 人员列表界面:
    人员列表
  • 新增弹窗界面:
    新增弹窗
  • 下拉选单界面:
    下拉选单

解决方案

问题核心是:你调用的setState是页面_PersonState的,它只会触发页面重建,但ModalBottomSheet是独立的路由组件,页面的setState无法更新弹窗UI。

解决方法是用StatefulBuilder包裹弹窗内需要更新的部分,让弹窗拥有独立的状态管理能力:

修改_create方法如下:

Future<void> _create() async {
  String? localDeptName = null; // 用局部变量管理弹窗内选中值,避免和页面变量耦合
  await showModalBottomSheet(
    isScrollControlled: true,
    context: context,
    builder: (BuildContext ctx) {
      return StatefulBuilder( // 新增StatefulBuilder,提供弹窗内部的setState
        builder: (ctx, setState) {
          return Padding(
            padding: EdgeInsets.only(top: 20, left: 20, right: 20, bottom: MediaQuery.of(ctx).viewInsets.bottom + 20),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                TextField(controller: _nameController, decoration: InputDecoration(labelText: 'person_name'.tr())),
                const SizedBox(height: 10),
                StreamBuilder<QuerySnapshot>(
                  stream: _department.snapshots(),
                  builder: (context, snapshot) {
                    if (!snapshot.hasData) {
                      return Text("loading").tr();
                    } else {
                      List<DropdownMenuItem<String>> departments = []; // 指定泛型避免类型警告
                      for (var snap in snapshot.data!.docs) {
                        String deptName = snap.get('departmentName');
                        departments.add(DropdownMenuItem(child: Text(deptName), value: deptName));
                      }
                      return Row(
                        mainAxisAlignment: MainAxisAlignment.center,
                        children: <Widget>[
                          Expanded(
                            child: DropdownButton<String>( // 指定泛型
                              value: localDeptName,
                              items: departments,
                              onChanged: (newValue) {
                                setState(() { // 调用弹窗内部的setState更新UI
                                  localDeptName = newValue;
                                  print('$localDeptName is selected');
                                });
                              },
                              isExpanded: true,
                            ),
                          ),
                        ],
                      );
                    }
                  },
                ),
                ElevatedButton(
                  child: const Text('save').tr(),
                  onPressed: () async {
                    final String name = _nameController.text;
                    if (name.isNotEmpty) { // 判断非空更符合实际场景
                      await _person.add({"personName": name, 'departmentName': localDeptName});
                      _nameController.text = '';
                      Navigator.of(ctx).pop();
                    }
                  },
                )
              ],
            ),
          );
        },
      );
    },
  );
}

额外优化点:

  1. 用局部变量localDeptName管理弹窗状态,避免和页面变量耦合
  2. 给DropdownButton和DropdownMenuItem指定<String>泛型,消除类型警告
  3. 简化部门列表遍历逻辑,用for-in替代索引循环
  4. 保存时判断name.isNotEmpty,比name != null更准确(TextField的text永远不会为null)

内容的提问来源于stack exchange,提问作者Ufuk Ugur

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.01 13:51:30