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

如何在Flutter中用flutter_riverpod实现搜索框并修复报错

Flutter Riverpod 搜索框实现问题排查与解决方案

我使用flutter_riverpod包实现搜索框时出现报错,相关代码如下:

UI 代码

TextFormField(
  …
  onChanged: (search) => controller.updateSearch(search),
  onSaved: (search) {
    search == null ? null : controller.updateSearch(search);
  },
),
itemBuilder: (context, index) {
 if (mounted) {
    return name.contains(state.search) ? ListTile(title: Text(name)) : Container();
  }

  return Container();
},

Controller 代码

class Controller extends StateNotifier<State> {
Controller() : super(State());

void updateSearch(String search) => state = state.copyWith(search: search);

}

final controllerProvider = StateNotifierProvider.autoDispose< Controller, State>((ref) {
  return Controller();
});

State 代码

class State {
  State({this.search = "", this.value = const AsyncValue.data(null)});

  final String search;
  final AsyncValue<void> value;
  bool get isLoading => value.isLoading;

  State copyWith({String? search, AsyncValue<void>? value}) {
    return State(search: search ?? this.search, value: value ?? this.value);
  }
}

现有代码存在的问题

  1. State类未重写相等性判断:StateNotifier依赖不可变状态的==和hashCode判断状态是否更新,现有State类未实现这两个方法,可能导致状态更新后UI不刷新,或触发不必要的重建。
  2. 列表构建逻辑不合理:在itemBuilder中返回空Container会导致列表出现空白占位,应该先过滤数据再构建列表;mounted判断在此场景下无意义,反而可能阻碍状态更新后的UI重建。
  3. TextFormField的onSaved逻辑冗余:空值判断可简化,无需返回null。

基于Flutter Riverpod的正确实现

1. 修正State类(重命名为SearchState避免冲突)

class SearchState {
  SearchState({this.search = "", this.value = const AsyncValue.data(null)});

  final String search;
  final AsyncValue<void> value;
  bool get isLoading => value.isLoading;

  SearchState copyWith({String? search, AsyncValue<void>? value}) {
    return SearchState(
      search: search ?? this.search,
      value: value ?? this.value,
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is SearchState &&
          runtimeType == other.runtimeType &&
          search == other.search &&
          value == other.value;

  @override
  int get hashCode => search.hashCode ^ value.hashCode;
}

2. 修正Controller类(重命名为SearchController)

class SearchController extends StateNotifier<SearchState> {
  SearchController() : super(SearchState());

  void updateSearch(String search) => state = state.copyWith(search: search);
}

final searchControllerProvider = StateNotifierProvider.autoDispose<SearchController, SearchState>((ref) {
  return SearchController();
});

3. 优化UI实现

class SearchPage extends ConsumerWidget {
  const SearchPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final searchState = ref.watch(searchControllerProvider);
    final searchController = ref.read(searchControllerProvider.notifier);
    // 假设原始数据列表为names
    final List<String> names = ["Apple", "Banana", "Orange", "Grape"];
    final filteredNames = names.where((name) => name.contains(searchState.search)).toList();

    return Scaffold(
      appBar: AppBar(title: const Text("搜索")),
      body: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(hintText: "输入搜索内容"),
            onChanged: searchController.updateSearch,
            onSaved: (search) {
              if (search != null) searchController.updateSearch(search);
            },
          ),
          Expanded(
            child: ListView.builder(
              itemCount: filteredNames.length,
              itemBuilder: (context, index) {
                return ListTile(title: Text(filteredNames[index]));
              },
            ),
          ),
        ],
      ),
    );
  }
}

报错原因推测

如果未重写State类的==和hashCode,StateNotifier会认为新生成的State与旧State相等,不会通知UI更新;另外,itemBuilder中返回空容器可能导致列表布局异常,引发渲染报错。

不依赖第三方包、不使用setState的实现方案

可以使用Flutter原生的ValueNotifier或ChangeNotifier实现:

方案1:使用ValueNotifier

class SearchPage extends StatefulWidget {
  const SearchPage({super.key});

  @override
  State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  final ValueNotifier<String> _searchNotifier = ValueNotifier("");
  final List<String> _names = ["Apple", "Banana", "Orange", "Grape"];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("搜索")),
      body: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(hintText: "输入搜索内容"),
            onChanged: (text) => _searchNotifier.value = text,
          ),
          Expanded(
            child: ValueListenableBuilder<String>(
              valueListenable: _searchNotifier,
              builder: (context, searchText, child) {
                final filteredNames = _names.where((name) => name.contains(searchText)).toList();
                return ListView.builder(
                  itemCount: filteredNames.length,
                  itemBuilder: (context, index) {
                    return ListTile(title: Text(filteredNames[index]));
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

方案2:使用ChangeNotifier

class SearchModel extends ChangeNotifier {
  String _search = "";
  String get search => _search;

  void updateSearch(String text) {
    _search = text;
    notifyListeners();
  }
}

class SearchPage extends StatefulWidget {
  const SearchPage({super.key});

  @override
  State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  final SearchModel _searchModel = SearchModel();
  final List<String> _names = ["Apple", "Banana", "Orange", "Grape"];

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("搜索")),
      body: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(hintText: "输入搜索内容"),
            onChanged: _searchModel.updateSearch,
          ),
          Expanded(
            child: AnimatedBuilder(
              animation: _searchModel,
              builder: (context, child) {
                final filteredNames = _names.where((name) => name.contains(_searchModel.search)).toList();
                return ListView.builder(
                  itemCount: filteredNames.length,
                  itemBuilder: (context, index) {
                    return ListTile(title: Text(filteredNames[index]));
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 12:45:17