Search code examples
flutterdartriverpodgoogle-places-autocompleteflutter-change-notifier-provider

UI doesn't update after changing state of riverpod notifier


I am using a SearchAnchor with a SearchBar and I'm trying to show autocomplete suggestions retrieved by the Google Places API (new). The api call works and returns the result as well as its parsing. The UI list stays always empty though as if it is just receiving the initialState []. What is it that I need to change?

I am using this riverpod notifier

@riverpod
class QueryService extends _$QueryService {
  final initialState = <Suggestion>[];

  @override
  Future<List<Suggestion>> build() async {
    return initialState;
  }

  Future<void> searchForQuery(String query) async {
    if (query.isEmpty) {
      state = AsyncData(initialState);
    } else {
      final Position(:latitude, :longitude) = await ref.read(positionProvider.future);
      final data = jsonEncode({
        "input": query,
        "includeQueryPredictions": true,
        "locationBias": {
          "circle": {
            "center": {"latitude": latitude, "longitude": longitude},
          },
        },
      });
      final uri = Uri.https(_baseUrl, 'v1/places:autocomplete');
      final response = await http.post(uri, headers: _headers, body: data);
      final autocompleteResponse = AutocompleteResponse.fromJson(jsonDecode(response.body));
      state = AsyncData(autocompleteResponse.suggestions);
    }
  }
}

This is the search anchor code

class MapSearchBar extends ConsumerStatefulWidget {
  const MapSearchBar({ super.key });

  @override
  ConsumerState<MapSearchBar> createState() => _MapSearchBarState();
}

class _MapSearchBarState extends ConsumerState<MapSearchBar> {
  final _searchController = SearchController();

  @override
  void initState() {
    super.initState();
    _searchController.addListener(() {
      ref.read(queryServiceProvider.notifier).searchForQuery(_searchController.text);
    });
  }

  @override
  Widget build(BuildContext context) {
    final suggestionsState = ref.watch(queryServiceProvider);

    return SearchAnchor(
      searchController: _searchController,
      viewBackgroundColor: kWhite,
      viewSurfaceTintColor: kWhite,
      dividerColor: kGrey,
      builder: (context, controller) => SearchBar(
        controller: controller,
        onChanged: (value) {
          controller.openView();
        },
      ),
      suggestionsBuilder: (context, controller) {
        if (!controller.isOpen) controller.openView();

        return suggestionsState.when(
          data: (suggestions) => suggestions.map((suggestion) {
            return ListTile(
              title: Text(suggestion.structuredFormat.mainText.text),
              onTap: () {},
            );
          }),
          error: (_, __) => [Text('error')],
          loading: () => [Text('loading')],
        );
      },
    );
  }
}

Also this is the freezed model

@freezed
class AutocompleteResponse with _$AutocompleteResponse {
  const factory AutocompleteResponse(@SuggestionConverter() List<Suggestion> suggestions) = AutocompleteResponseData;

  factory AutocompleteResponse.fromJson(Map<String, dynamic> json) => _$AutocompleteResponseFromJson(json);
}

@freezed
sealed class Suggestion with _$Suggestion {
  const factory Suggestion.place({
    required String place,
    required String placeId,
    required TextMatchData text,
    required StructuredFormatData structuredFormat,
    required List<PlaceType> types,
  }) = PlaceSuggestion;

  @SuggestionConverter()
  const factory Suggestion.query({
    required StructuredFormatData structuredFormat,
    required TextMatchData text,
  }) = QuerySuggestion;

  factory Suggestion.fromJson(Map<String, dynamic> json) => _$SuggestionFromJson(json);
}

class SuggestionConverter implements JsonConverter<Suggestion, Map<String, dynamic>> {
  const SuggestionConverter();

  @override
  Suggestion fromJson(Map<String, dynamic> json) {
    if (json.containsKey(placePredictionKey)) {
      return PlaceSuggestion.fromJson(json[placePredictionKey]);
    } else if (json.containsKey(queryPredictionKey)) {
      return QuerySuggestion.fromJson(json[queryPredictionKey]);
    } else {
      throw Exception('Could not determine the constructor for mapping prediction from JSON');
    }
  }

  @override
  Map<String, dynamic> toJson(Suggestion data) =>
      data.map(place: (p) => {placePredictionKey: p.toJson()}, query: (q) => {queryPredictionKey: q.toJson()});
}

Solution

  • The problem is that the suggestionsBuilder callback won't be reactive to the changes that trigger the build method (in other word, when the build method is retriggered because of the queryServiceProvider that is being watched has a value change, the suggestionsBuilder callback won't be called again).

    suggestionsBuilder is an asynchronous callback, which means you can use ref.read on the future value of the provider.

    suggestionsBuilder: (context, controller) {
      final suggestions = await ref.read(queryServiceProvider.future);
      // ...
    

    Additionally, it should be fine to call the searchForQuery method inside this callback. This approach is also what's demonstrated on the 4th example from the documentation, in which the fetch method is called inside the suggestionsBuilder callback, before returning the widget.

    With this approach, you would need to remove _searchController.addListener from the initState and the suggestionsBuilder should look like this:

    suggestionsBuilder: (context, controller) {
      ref.read(queryServiceProvider.notifier).searchForQuery(controller.text);
      final suggestions = await ref.read(queryServiceProvider.future);
      // ...