Skip to main content
GuidesAPI ReferenceExamplesFAQ

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

main.dart
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.

search_page.dart
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),
)!,
gaplessPlayback: true,
)
: 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;
}
}