How to correctly use ChangeNotifier with SearchDelegate?

I have something that looks like this:

class SearchNotifier with ChangeNotifier {
  List<String> results;

  Future<void> search(String query) async {
    results = await API.search(query);
    notifyListeners();
  }
}

And in my SearchDelegate:

Widget buildSuggestions(BuildContext context) {
  final searchNotifier = Provider.of<SearchNotifier>(context);
  searchNotifier.search(query);
  ...
}

When the results update, SearchNotifier updates its listeners, SearchDelegate is rebuild, buildSuggestions is called and search is called again, entering in a loop.

Is there a way of doing searchNotifier.search(query) outside a build method? Maybe somehow I can add a listener to SearchDelegate _queryTextController?

I'm using provider to inject my SearchNotifier, so wherever call search we need to have access to the context.


Solution 1: Sergey

I made it differently, but I am also using the Provider in my app. So my custom search delegate looks like this:

 class ExerciseSearchDelegate extends SearchDelegate {
  @override
  List<Widget> buildActions(BuildContext context) {
    return [
      IconButton(
        icon: Icon(Icons.clear),
        onPressed: () {
          query = '';
        },
      ),
    ];
  }

  @override
  Widget buildLeading(BuildContext context) {
    return IconButton(
      icon: Icon(Icons.arrow_back),
      onPressed: () {
        close(context, null);
      },
    );
  }

  @override
  Widget buildResults(BuildContext context) {
    // ExeciseList is a widget which accepts a query as optional parameter
    return ExerciseList(
      query: query,
    );
  }

  @override
  Widget buildSuggestions(BuildContext context) {
    return Column();
  }

  @override
  ThemeData appBarTheme(BuildContext context) {
    assert(context != null);
    final ThemeData theme = Theme.of(context);
    assert(theme != null);
    return theme;
  }
}

and my ExerciseList looks like this:

class ExerciseList extends StatelessWidget {
  final String query;
  const ExerciseList({this.query, Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    final _model = Provider.of<GlobalModel>(context);

    List<ExerciseModel> _results = query != null
        ? _model.exercises
            .where((ex) => ex.name.toLowerCase().contains(query.toLowerCase()))
            .toList()
        : _model.exercises;

    return ListView.builder(
      itemCount: _results.length,
      itemBuilder: (context, position) {
        var exercise = _results[position];
        return MultiProvider(
          providers: [
            ChangeNotifierProvider.value(notifier: exercise),
          ],
          child: ExerciseListItem(),
        );
      },
    );
  }
}

Hope this helps. If something is not clear, please let me know. Thank you!


Solution 2: lgvaz

I solved the issue by returning the search results directly from the search method and then using a FutureBuilder.

class SearchNotifier with ChangeNotifier {
  Future<List<String>> search(String query) async {
    return await API.search(query);
  }
}

Widget buildSuggestions(BuildContext context) {
  final searchNotifier = Provider.of<SearchNotifier>(context);
  results = searchNotifier.search(query);
  // FutureBuilder on results
  ...
}

SearchNotifier does not need to be a ChangeNotifier anymore, since the origin of the issue was notifyListeners.