Search Category
In this guide, you will learn how to integrate map functionality and perform searches for landmarks.
How It Works
This example demonstrates the following key features:
- Search for landmarks based on specific categories.
- Filter landmarks displayed on map by categories, making searches more targeted.
![]() | ![]() |
---|---|
Initial map view | Search Category Page |
![]() | ![]() |
---|---|
Selected category for searching | Found landmarks by their category |
UI and Map Integration
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Search Category',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late GemMapController _mapController;
void dispose() {
GemKit.release();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.deepPurple[900],
title: const Text("Search Category", style: TextStyle(color: Colors.white)),
actions: [
IconButton(
onPressed: () => _onSearchButtonPressed(context),
icon: const Icon(Icons.search, color: Colors.white),
),
],
),
body: GemMap(
key: ValueKey("GemMap"),
onMapCreated: _onMapCreated,
appAuthorization: projectApiToken,
),
);
}
void _onMapCreated(GemMapController controller) {
_mapController = controller;
}
// Custom method for navigating to search screen
void _onSearchButtonPressed(BuildContext context) async {
// Taking the coordinates at the center of the screen as reference coordinates for search.
final x = MediaQuery.of(context).size.width / 2;
final y = MediaQuery.of(context).size.height / 2;
final mapCoords = _mapController.transformScreenToWgs(XyType(x: x.toInt(), y: y.toInt()));
// Navigating to search screen. The result will be the selected search result (Landmark)
final result = await Navigator.of(context).push(MaterialPageRoute<dynamic>(
builder: (context) => SearchPage(
controller: _mapController,
coordinates: mapCoords!,
),
));
if (result is Landmark) {
// Activating the highlight
_mapController.activateHighlight([result], renderSettings: HighlightRenderSettings());
// Centering the map on the desired coordinates
_mapController.centerOnCoordinates(result.coordinates);
}
}
}
Define Search Functionality
Implement the SearchPage widget that allows users to search for landmarks.
class SearchPage extends StatefulWidget {
final GemMapController controller;
final Coordinates coordinates;
// Method to get all the generic categories
final categories = GenericCategories.categories;
SearchPage({super.key, required this.controller, required this.coordinates});
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
final TextEditingController _textController = TextEditingController();
List<Landmark> landmarks = [];
List<LandmarkCategory> selectedCategories = [];
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: _onLeadingPressed,
icon: const Icon(CupertinoIcons.arrow_left),
),
title: const Text("Search Category"),
backgroundColor: Colors.deepPurple[900],
foregroundColor: Colors.white,
actions: [
if (landmarks.isEmpty)
IconButton(
onPressed: () => _onSubmitted(_textController.text),
icon: const Icon(Icons.search),
),
],
),
body: Column(
children: [
if (landmarks.isEmpty)
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _textController,
cursorColor: Colors.deepPurple[900],
decoration: const InputDecoration(
hintText: 'Enter text',
hintStyle: TextStyle(color: Colors.black),
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(
color: Colors.deepPurple,
width: 2.0,
),
),
),
),
),
if (landmarks.isEmpty)
Expanded(
child: ListView.separated(
padding: EdgeInsets.zero,
itemCount: widget.categories.length,
controller: ScrollController(),
separatorBuilder: (context, index) => const Divider(indent: 50, height: 0),
itemBuilder: (context, index) {
return CategoryItem(
onTap: () => _onCategoryTap(index),
category: widget.categories[index],
categoryIcon: widget.categories[index].img,
);
},
),
),
if (landmarks.isNotEmpty)
Expanded(
child: ListView.separated(
padding: EdgeInsets.zero,
itemCount: landmarks.length,
controller: ScrollController(),
separatorBuilder: (context, index) => const Divider(indent: 50, height: 0),
itemBuilder: (context, index) {
final lmk = landmarks.elementAt(index);
return SearchResultItem(landmark: lmk);
},
),
),
const SizedBox(height: 5),
],
),
);
}
int? _isCategorySelected(LandmarkCategory category) {
for (int index = 0; index < selectedCategories.length; index++) {
if (category.id == selectedCategories[index].id) {
return index;
}
}
return null;
}
void _onSubmitted(String text) {
// Setting the preferences so the results are only from the selected categories
SearchPreferences preferences = SearchPreferences(
maxMatches: 40,
allowFuzzyResults: false,
searchMapPOIs: true,
searchAddresses: false,
);
// Adding in search preferences the selected categories
for (final category in selectedCategories) {
preferences.landmarks.addStoreCategoryId(
category.landmarkStoreId,
category.id,
);
}
search(text, widget.coordinates, preferences);
}
late Completer<List<Landmark>> completer;
// Search method
Future<void> search(
String text,
Coordinates coordinates,
SearchPreferences preferences,
) async {
completer = Completer<List<Landmark>>();
// Calling the search around position SDK method.
// (err, results) - is a callback function that calls when the computing is done.
// err is an error code, results is a list of landmarks
SearchService.searchAroundPosition(
coordinates,
preferences: preferences,
textFilter: text,
(err, results) async {
// If there is an error or there aren't any results, the method will return an empty list.
if (err != GemError.success) {
completer.complete([]);
return;
}
if (!completer.isCompleted) completer.complete(results);
},
);
final result = await completer.future;
setState(() {
landmarks = result;
});
}
void _onLeadingPressed() {
if (landmarks.isNotEmpty) {
landmarks.clear();
_textController.clear();
selectedCategories.clear();
setState(() {});
return;
}
Navigator.pop(context);
}
void _onCategoryTap(int index) {
int? categoryIndex = _isCategorySelected(widget.categories[index]);
if (categoryIndex != null) {
selectedCategories.removeAt(categoryIndex);
} else {
selectedCategories.add(widget.categories[index]);
}
}
}
// Class for the categories.
class CategoryItem extends StatefulWidget {
final LandmarkCategory category;
final Img categoryIcon;
final VoidCallback onTap;
const CategoryItem({
super.key,
required this.category,
required this.onTap,
required this.categoryIcon,
});
State<CategoryItem> createState() => _CategoryItemState();
}
class _CategoryItemState extends State<CategoryItem> {
bool _isSelected = false;
Widget build(BuildContext context) {
return ListTile(
onTap: () {
widget.onTap();
setState(() {
_isSelected = !_isSelected;
});
},
leading: Container(
padding: const EdgeInsets.all(8),
child: widget.categoryIcon.isValid
? Image.memory(widget.categoryIcon.getRenderableImageBytes(size: Size(50, 50))!)
: SizedBox(),
),
title: Text(
widget.category.name,
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
trailing: (_isSelected)
? const SizedBox(
width: 50,
child: Icon(Icons.check, color: Colors.grey),
)
: null,
);
}
}
// Class for the search results.
class SearchResultItem extends StatefulWidget {
final Landmark landmark;
const SearchResultItem({super.key, required this.landmark});
State<SearchResultItem> createState() => _SearchResultItemState();
}
class _SearchResultItemState extends State<SearchResultItem> {
Widget build(BuildContext context) {
return ListTile(
onTap: () => Navigator.of(context).pop(widget.landmark),
leading: Container(
padding: const EdgeInsets.all(8),
child: widget.landmark.img.isValid
? Image.memory(widget.landmark.img.getRenderableImageBytes(size: Size(50, 50))!)
: SizedBox(),
),
title: Text(
widget.landmark.name,
overflow: TextOverflow.fade,
style: const TextStyle(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.w400,
),
maxLines: 2,
),
subtitle: Text(
"${widget.landmark.getFormattedDistance()} ${widget.landmark.getAddress()}",
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.black,
fontSize: 14,
fontWeight: FontWeight.w400,
),
),
);
}
}
// Define an extension for landmark for formatting the address and the distance.
extension LandmarkExtension on Landmark {
String getAddress() {
final addressInfo = address;
final street = addressInfo.getField(AddressField.streetName);
final city = addressInfo.getField(AddressField.city);
final country = addressInfo.getField(AddressField.country);
if (street == null && city == null && country == null) {
return 'Address not available';
}
return " ${street ?? ""} ${city ?? ""} ${country ?? ""}";
}
String getFormattedDistance() {
String formattedDistance = '';
double distance = (extraInfo.getByKey(PredefinedExtraInfoKey.gmSearchResultDistance) / 1000) as double;
formattedDistance = "${distance.toStringAsFixed(0)}km";
return formattedDistance;
}
}
Flutter Examples
Maps SDK for Flutter Examples can be downloaded or cloned with Git.