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()});
}
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);
// ...