Skip to content

Search Category

In this guide, you will learn how to integrate map functionality and perform searches for landmarks.

Search Category - example Flutter screenshot

Setup

First, get an API key token, see the Getting Started guide.

Prerequisites

Make sure you completed the Environment Setup - Flutter Examples guide before starting this guide.

Build and Run

Go to the search_category directory within the Flutter examples directory.

Note - the gem_kit directory containing the Maps SDK for Flutter should be in the plugins directory of the example, e.g. example_pathname/plugins/gem_kit - see the environment setup guide above.

Run: flutter pub get

Configure the native parts:

First, verify that the ANDROID_SDK_ROOT environment variable is set to the root path of your android SDK.

In android/build.gradle add the maven block as shown, within the allprojects block, for both debug and release builds:

allprojects {
    repositories {
        google()
        mavenCentral()
        maven {
           url "${rootDir}/../plugins/gem_kit/android/build"
        }
    }
}

in android/app/build.gradle within the android block, in the defaultConfig block, the android SDK version minSdk must be set as shown below.

Additionally, for release builds, in android/app/build.gradle, within the android block, add the buildTypes block as shown:

Replace example_pathname with the actual project pathname

android {
    defaultConfig {
        applicationId "com.magiclane.gem_kit.examples.example_pathname"
        minSdk 21
        targetSdk flutter.targetSdk
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
    buildTypes {
        release {
            minifyEnabled false
            shrinkResources false

            // TODO: Add your own signing config for the release build.
            // Signing with the debug keys for now, so `flutter run --release` works.
            signingConfig signingConfigs.debug
        }
    }
}

In the ios/Podfile configuration file, at the top, set the minimum ios platform version to 14 like this:

platform :ios, '14.0'

We recommend you to run these commands after you copy the gem_kit into your project: |flutter clean |flutter pub get |and |cd ios |pod install

Then run the project:

flutter run --debug
or
flutter run --release

Import Necessary Packages

First, import the required packages in your Dart code.

import 'package:gem_kit/core.dart';
import 'package:gem_kit/map.dart';

import 'search_page.dart';

import 'package:flutter/material.dart';

App entry and initialization

const projectApiToken = String.fromEnvironment('GEM_TOKEN');

void main() {
  runApp(const MyApp());
}

This code initializes the projectApiToken with the required authorization token and launches the app.

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.

Search Category - example Flutter screenshot

Search Category - example Flutter screenshot

Search Category - example Flutter screenshot

Build the Main Application

Define the main application widget, MyApp.

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Search Category',
      home: MyHomePage(),
    );
  }
}

Handle Map and Search Functionality

Create the stateful widget, MyHomePage, which will handle the map and search functionality.

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key});

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

Define State Variables and Methods

Within _MyHomePageState, define the necessary state variables and methods to manage the map and perform searches.

class _MyHomePageState extends State<MyHomePage> {
  late GemMapController _mapController;

  @override
  void dispose() {
    GemKit.release();
    super.dispose();
  }

  @override
  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(
        onMapCreated: _onMapCreated,
        : 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});

  @override
  State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  final TextEditingController _textController = TextEditingController();
  List<Landmark> landmarks = [];
  List<LandmarkCategory> selectedCategories = [];

  @override
  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].getImage(size: Size(200, 200)),
                  );
                },
              ),
            ),
          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),
        ],
      ),
    );
  }

  // Search method
  Future<void> search(String text, Coordinates coordinates, SearchPreferences preferences) async {
    // Implementation of search functionality
  }

  void _onLeadingPressed() {
    // Handle back navigation
  }

  void _onCategoryTap(int index) {
    // Handle category selection
  }
}

// Class for the categories.
class CategoryItem extends StatefulWidget {
  final LandmarkCategory category;
  final Uint8List categoryIcon;
  final VoidCallback onTap;

  const CategoryItem({
    super.key,
    required this.category,
    required this.onTap,
    required this.categoryIcon
  });

  @override
  State<CategoryItem> createState() => _CategoryItemState();
}

class _CategoryItemState extends State<CategoryItem> {
  bool _isSelected = false;

  @override
  Widget build(BuildContext context) {
    return ListTile(
        onTap: () {
          widget.onTap();
          setState(() {
            _isSelected = !_isSelected;
          });
        },
        leading: Container(
          padding: const EdgeInsets.all(8),
          width: 50,
          height: 50,
          child: Image.memory(widget.categoryIcon),
        ),
        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});

  @override
  State<SearchResultItem> createState() => _SearchResultItemState();
}

class _SearchResultItemState extends State<SearchResultItem> {
  @override
  Widget build(BuildContext context) {
    return ListTile(
      onTap: () => Navigator.of(context).pop(widget.landmark),
      leading: Container(
        padding: const EdgeInsets.all(8),
        width: 50,
        child: Image.memory(
          widget.landmark.getImage(),
        ),
      ),
      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),
      ),
    );
  }
}

Flutter Examples

Maps SDK for Flutter Examples can be downloaded or cloned with Git