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

Flutter DraggableScrollableSheet内嵌搜索栏无法实时过滤数据问题

问题根因

你遇到的搜索不实时生效问题,本质是showModalBottomSheet默认属于独立的路由上下文,你在父组件调用setState只能触发父组件的build方法重绘,不会主动刷新底部弹窗内的视图,只有当键盘收起触发父页面布局更新时,底部弹窗才会同步最新的foundCity数据,因此出现了延迟更新的现象。

解决方案

方案1:将底部弹窗抽为独立StatefulWidget(推荐)

将底部选择城市的逻辑独立封装,让弹窗自己管理搜索状态、城市列表数据,选中后通过回调将结果返回给父组件,可完全解决状态同步问题。
父组件中触发弹窗的代码修改如下:

GestureDetector(
  onTap: () => showModalBottomSheet(
    backgroundColor: Colors.transparent,
    isScrollControlled: true,
    context: context,
    builder: (context) => CitySelectSheet(
      cityList: cites,
      onSelected: (city) {
        setState(() {
          selected = city;
        });
      },
    ),
  ),
  // 原有容器展示代码保持不变
)

独立的城市选择弹窗组件代码:

class CitySelectSheet extends StatefulWidget {
  final List<City> cityList;
  final Function(City) onSelected;
  const CitySelectSheet({Key? key, required this.cityList, required this.onSelected}) : super(key: key);

  @override
  State<CitySelectSheet> createState() => _CitySelectSheetState();
}

class _CitySelectSheetState extends State<CitySelectSheet> {
  final TextEditingController _searchController = TextEditingController();
  List<City> _foundCity = [];

  @override
  void initState() {
    super.initState();
    _foundCity = widget.cityList;
    _searchController.addListener(() {
      setState(() {
        final keyword = _searchController.text.toLowerCase();
        _foundCity = widget.cityList.where((city) => city.name.toLowerCase().contains(keyword)).toList();
      });
    });
  }

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: () => Navigator.of(context).pop(),
      child: DraggableScrollableSheet(
        initialChildSize: 0.9,
        builder: (_, controller) => Container(
          padding: const EdgeInsets.fromLTRB(10, 10, 10, 0),
          decoration: const BoxDecoration(
            color: Colors.white,
            borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
          ),
          child: Column(
            children: [
              Divider(
                thickness: 4,
                color: Colors.grey.shade300,
                endIndent: 170,
                indent: 170,
              ),
              const Padding(
                padding: EdgeInsets.only(bottom: 15, top: 5),
                child: Text("Select city",
                    style: TextStyle(
                        fontSize: 18,
                        color: Colors.black,
                        fontWeight: FontWeight.bold)),
              ),
              Padding(
                padding: const EdgeInsets.only(bottom: 10),
                child: TextFormField(
                  controller: _searchController,
                  keyboardType: TextInputType.name,
                  style: TextStyle(fontSize: 16 * fontScale(context)),
                  decoration: InputDecoration(
                    border: OutlineInputBorder(
                        borderRadius:
                        BorderRadius.circular(7 * screenScale(context))),
                    contentPadding:
                    EdgeInsets.only(top: 10 * screenScale(context)),
                    hintText: 'Search',
                    prefixIcon: const Icon(Ionicons.search_outline),
                  ),
                ),
              ),
              Expanded(
                child: _foundCity.isNotEmpty
                    ? ListView.builder(
                  itemCount: _foundCity.length,
                  itemBuilder: (context, index) {
                    final city = _foundCity[index];
                    return ListTile(
                      title: Text(city.name),
                      onTap: () {
                        Navigator.of(context).pop();
                        widget.onSelected(city);
                      },
                    );
                  },
                )
                    : const Padding(
                  padding: EdgeInsets.only(top: 50),
                  child: Text(
                    "No data.",
                    style: TextStyle(fontSize: 16),
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

方案2:使用ValueNotifier局部刷新(无需拆分组件)

如果你不想拆分原有组件结构,可通过ValueNotifier实现局部数据监听刷新,不需要调用父组件setState也能更新弹窗内的列表:

  1. 首先将foundCity修改为ValueNotifier类型,并修改初始化逻辑:
// 原有定义 List<City> foundCity = []; 替换为下方代码
final ValueNotifier<List<City>> foundCity = ValueNotifier([]);

@override
void initState() {
  super.initState();
  foundCity.value = cites;
}
  1. 修改buildSheet方法内的搜索输入回调和列表渲染逻辑:
// TextFormField的onChanged修改为直接更新foundCity的value,不需要调用setState
onChanged: (value) {
  foundCity.value = cites
      .where((city) => city.name.toLowerCase().contains(value.toLowerCase()))
      .toList();
},

// 列表部分用ValueListenableBuilder包裹,监听foundCity的变化自动刷新
Expanded(
  child: ValueListenableBuilder<List<City>>(
    valueListenable: foundCity,
    builder: (context, list, child) {
      return list.isNotEmpty
          ? ListView.builder(
              itemCount: list.length,
              itemBuilder: (context, index) {
                final city = list[index];
                return ListTile(
                  title: Text(city.name),
                  onTap: () {
                    Navigator.of(context).pop();
                    setState(() {
                      selected = city;
                    });
                  },
                );
              },
            )
          : const Padding(
              padding: EdgeInsets.only(top: 50),
              child: Text(
                "No data.",
                style: TextStyle(fontSize: 16),
              ),
            );
    },
  ),
)
  1. 组件销毁时释放ValueNotifier避免内存泄漏:
@override
void dispose() {
  _search.dispose();
  foundCity.dispose();
  super.dispose();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.26 16:36:04