Neshan Maps Flutter

A Flutter SDK for integrating Neshan Maps into your Flutter applications.

Features

  • Cross-platform — works on Android, iOS, Web, and all other Flutter-supported platforms
  • Interactive Map — pan, zoom, multiple map styles, traffic layer, and POI toggles
  • Markers & Controller — place markers declaratively or manage them at runtime via NeshanMapController
  • Shapes — draw polygons, polylines, and circles with full styling control and tap events
  • Location Picker — draggable centre-pin with automatic reverse-geocoding and built-in place search
  • Customisable UI — override the address bar, confirm button, and centre marker with your own widgets

Screenshots

NeshanMap NeshanLocationPicker Location Search Shapes
Map Picker Search Shapes

Table of Contents

Getting API Keys

Register at platform.neshan.org and create these keys:

Key Used for Docs
mapKey Displaying the map. Required for both NeshanMap and NeshanLocationPicker.
reverseGeocodingApiKey Converting coordinates to an address string. Required only for NeshanLocationPicker. Reverse Geocoding API
searchApiKey Searching for places by name. Optional for NeshanLocationPicker. Search API

Important notes

The mapKey must be a Web key. This package uses the Neshan Web SDK on all platforms (WebView on mobile, <iframe> on web), so generate a Web key. Android/iOS keys will not work.

Do not restrict the allowed domain / IP. Requests come from a device WebView, not a fixed backend domain/IP. Restricting domain or IP can break map loading.

Keep keys out of your source code. Never commit keys. Prefer fetching them from your backend at runtime instead of bundling them in the app.

Installation

Run this command:

flutter pub add neshan_maps_flutter

This will add the latest version to your pubspec.yaml and install it.

Quick Start

NeshanMap — minimal usage

import 'package:neshan_maps_flutter/map.dart';

NeshanMap(
  mapKey: 'YOUR_MAP_KEY',
)

That's it. The map opens at the default location and zoom level.

NeshanLocationPicker — minimal usage

import 'package:neshan_maps_flutter/location_picker.dart';

NeshanLocationPicker(
  mapKey: 'YOUR_MAP_KEY',
  reverseGeocodingApiKey: 'YOUR_REVERSE_GEOCODING_KEY',
  onLocationAccepted: (position, address) {
    print('Selected: $address at ${position.latitude}, ${position.longitude}');
  },
)

As the user pans, the address bar at the top updates automatically. Tapping the confirm button triggers onLocationAccepted.

Location Permission Setup

This step is optional. It is only required if you want to show the user's current location on the map via showCurrentLocationButton: true in NeshanMapConfig (the default).
If you set showCurrentLocationButton: false, you can skip this section entirely.

This package uses the geolocator plugin internally to access device location. Follow the platform-specific permission instructions in the geolocator documentation to add the required entries to your AndroidManifest.xml, Info.plist, and any other platform files.

NeshanMap — Full Reference

NeshanMap is a cross-platform widget that renders an interactive Neshan map. On mobile it uses a WebView; on web it uses an <iframe>.

Overlays on web

On web, the map sits in an HtmlElementView behind your Flutter widgets. Overlays such as buttons, sheets, or the location FAB may not receive taps—the underlying map can consume pointer events first. Wrap interactive overlays with PointerInterceptor from pointer_interceptor — add that package to your app’s pubspec.yaml so you can import it. See the package README for usage, including the intercepting flag when interception is only needed sometimes.

Complete example

import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:neshan_maps_flutter/map.dart';

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

  @override
  State<MapPage> createState() => _MapPageState();
}

class _MapPageState extends State<MapPage> {
  final _controller = NeshanMapController();

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NeshanMap(
        mapKey: 'YOUR_MAP_KEY',
        controller: _controller,
        config: NeshanMapConfig(
          initialCenter: LatLng(35.6892, 51.3890),
          initialZoom: 14.0,
          mapType: NeshanMapType.neshanVector,
          showTraffic: true,
        ),
        markers: const [
          NeshanMarker(
            id: 'hq',
            position: LatLng(35.6892, 51.3890),
            title: 'HQ',
            color: Colors.red,
          ),
        ],
        polygons: const [
          NeshanPolygon(
            id: 'zone1',
            coordinates: [
              LatLng(35.685, 51.385),
              LatLng(35.695, 51.385),
              LatLng(35.695, 51.395),
              LatLng(35.685, 51.395),
              LatLng(35.685, 51.385),
            ],
            fillColor: Colors.blue,
            fillOpacity: 0.3,
            strokeColor: Colors.blue,
          ),
        ],
        onLocationChanged: (lat, lng) {
          debugPrint('Centre moved to $lat, $lng');
        },
        onMarkerTapped: (markerId) {
          debugPrint('Marker tapped: $markerId');
        },
        onPolygonTapped: (polygonId) {
          debugPrint('Polygon tapped: $polygonId');
        },
        onError: (message, details) {
          debugPrint('Map error: $message');
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () async {
          await _controller.ready;
          _controller.moveToLocation(35.7448, 51.3753, zoom: 15);
        },
        child: const Icon(Icons.my_location),
      ),
    );
  }
}

NeshanMapConfig

Controls the initial viewport and style of the map. All parameters are optional.

NeshanMapConfig(
  initialCenter: LatLng(35.6892, 51.3890), // Default initial center
  initialZoom: 14.0,                        // Default: 12.0
  mapType: NeshanMapType.neshanVectorNight, // Default: neshanVector
  minZoom: 5.0,                             // Default: 2.0
  maxZoom: 18.0,                            // Default: 21.0
  showPoi: false,                           // Default: true
  showTraffic: true,                        // Default: false
  showCurrentLocationButton: false,         // Default: true
)

NeshanMapType

Value Description
NeshanMapType.neshanVector Default vector map
NeshanMapType.neshanVectorNight Vector map in night mode
NeshanMapType.neshanRaster Raster (tile) map
NeshanMapType.neshanRasterNight Raster map in night mode

NeshanMarker

Represents a pin on the map.

NeshanMarker(
  id: 'office',              // Required — unique identifier
  position: LatLng(35.6892, 51.3890), // Required
  color: Colors.deepPurple, // Optional — defaults to Neshan blue
  title: 'Our Office',      // Optional — popup text when tapped
  draggable: false,          // Optional — default false
)

NeshanPolygon

Represents a filled polygon on the map.

NeshanPolygon(
  id: 'zone1',              // Required — unique identifier
  coordinates: [            // Required — list of coordinates
    LatLng(35.700, 51.400),
    LatLng(35.710, 51.410),
    LatLng(35.720, 51.400),
    LatLng(35.700, 51.400), // Close the polygon
  ],
  fillColor: Colors.blue,   // Optional — defaults to blue
  fillOpacity: 0.3,         // Optional — 0.0 to 1.0, default 0.5
  strokeColor: Colors.blue, // Optional — defaults to black
  strokeWidth: 2.0,         // Optional — default 2.0
  strokeOpacity: 1.0,       // Optional — 0.0 to 1.0, default 1.0
)

NeshanPolyline

Represents a line or path on the map.

NeshanPolyline(
  id: 'route1',             // Required — unique identifier
  coordinates: [            // Required — list of coordinates
    LatLng(35.700, 51.400),
    LatLng(35.710, 51.410),
    LatLng(35.720, 51.420),
  ],
  color: Colors.red,        // Optional — defaults to blue
  width: 5.0,               // Optional — default 3.0
  opacity: 0.9,             // Optional — 0.0 to 1.0, default 1.0
  isDashed: false,          // Optional — default false (solid line)
)

NeshanCircle

Represents a circle overlay on the map.

NeshanCircle(
  id: 'coverage1',          // Required — unique identifier
  center: LatLng(35.6892, 51.3890), // Required — center point
  radius: 500,              // Optional — radius in meters, default 100
  fillColor: Colors.green,  // Optional — defaults to blue
  fillOpacity: 0.2,         // Optional — 0.0 to 1.0, default 0.3
  strokeColor: Colors.green, // Optional — defaults to black
  strokeWidth: 2.0,         // Optional — default 2.0
  strokeOpacity: 1.0,       // Optional — 0.0 to 1.0, default 1.0
)

NeshanMapController

Use NeshanMapController to control the map programmatically after it has loaded.

Important: Always await controller.ready before calling any method to ensure the map is fully initialised.

final controller = NeshanMapController();

// Pass to NeshanMap, then:
await controller.ready;

// Move camera
controller.moveToLocation(35.6892, 51.3890, zoom: 15.0);

// Change zoom only
controller.setZoom(12.0);

// Fit to bounding box (north, south, east, west)
controller.fitBounds(36.0, 35.0, 52.0, 51.0);

// Query state
final center = await controller.getCurrentLocation();
final zoom   = await controller.getCurrentZoom();

// Manage markers
controller.addMarker(NeshanMarker(id: 'new', position: LatLng(35.7, 51.4)));
controller.removeMarker('new');
controller.updateMarkers([/* replacement list */]);
controller.clearMarkers();

// Manage polygons
controller.addPolygon(NeshanPolygon(id: 'zone1', coordinates: [/* ... */]));
controller.removePolygon('zone1');
controller.updatePolygons([/* replacement list */]);
controller.clearPolygons();

// Manage polylines
controller.addPolyline(NeshanPolyline(id: 'route1', coordinates: [/* ... */]));
controller.removePolyline('route1');
controller.updatePolylines([/* replacement list */]);
controller.clearPolylines();

// Manage circles
controller.addCircle(NeshanCircle(id: 'poi1', center: LatLng(35.7, 51.4), radius: 500));
controller.removeCircle('poi1');
controller.updateCircles([/* replacement list */]);
controller.clearCircles();

// Cleanup
controller.dispose();

NeshanLocationPicker — Full Reference

The picker is ready to use out of the box — no extra configuration is needed beyond the API keys.
For deeper understanding of the underlying APIs, refer to the official Neshan documentation:

NeshanLocationPicker wraps NeshanMap and adds:

  • A centre-pin overlay indicating the selected location.
  • An address bar at the top that updates via reverse-geocoding as the user pans.
  • A search button (when searchApiKey is provided) that opens a full-screen search UI.
  • A confirm button that calls onLocationAccepted with the final LatLng and address string.

Complete example

import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import 'package:neshan_maps_flutter/location_picker.dart';

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: NeshanLocationPicker(
        mapKey: 'YOUR_MAP_KEY',
        reverseGeocodingApiKey: 'YOUR_REVERSE_GEOCODING_KEY',
        searchApiKey: 'YOUR_SEARCH_KEY', // Optional — enables search
        mapConfig: NeshanMapConfig(
          initialCenter: LatLng(35.6892, 51.3890),
          initialZoom: 14.0,
        ),
        locationPickerConfig: NeshanLocationPickerConfig(
          geocodingDebounce: Duration(milliseconds: 400),
          searchDebounce: Duration(milliseconds: 400),
        ),
        onLocationAccepted: (position, address) {
          Navigator.pop(context);
          print('Picked: $address (${position.latitude}, ${position.longitude})');
        },
        onAddressChanged: (address, response) {
          debugPrint('Address: $address | City: ${response.city}');
        },
        onApiError: (error) {
          debugPrint('API error [${error.statusCode}]: ${error.message}');
        },
      ),
    );
  }
}

NeshanLocationPickerConfig

Controls how aggressively the widget calls the Neshan APIs.

NeshanLocationPickerConfig(
  geocodingDebounce: Duration(milliseconds: 500), // Default: 300 ms
  searchDebounce: Duration(milliseconds: 400),    // Default: 300 ms
)

NeshanLocationPickerUiConfig — UI customization

Override any of the three overlay widgets with your own builders. Omitting a builder falls back to the default implementation.

NeshanLocationPicker(
  mapKey: 'YOUR_MAP_KEY',
  reverseGeocodingApiKey: 'YOUR_REVERSE_GEOCODING_KEY',
  onLocationAccepted: (position, address) { /* ... */ },
  locationPickerUiConfig: NeshanLocationPickerUiConfig(
    // Custom address bar
    addressDisplayBuilder: (context, data) {
      return Container(
        margin: const EdgeInsets.all(12),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
          boxShadow: [BoxShadow(blurRadius: 8, color: Colors.black12)],
        ),
        child: data.isLoading
            ? const LinearProgressIndicator()
            : Text(data.formattedAddress ?? 'Move the map to select a location'),
      );
    },

    // Custom confirm button
    acceptButtonBuilder: (context, data) {
      return Padding(
        padding: const EdgeInsets.all(16),
        child: ElevatedButton.icon(
          onPressed: data.onPressed,
          icon: const Icon(Icons.check),
          label: Text(data.isEnabled ? 'Confirm' : 'Loading…'),
        ),
      );
    },

    // Custom centre pin
    centerMarkerBuilder: (context) {
      return const Icon(Icons.location_pin, size: 48, color: Colors.red);
    },
  ),
)

The AddressDisplayData object passed to addressDisplayBuilder contains formattedAddress, fullResponse, isLoading, hasError, isSearchEnabled, and openSearchScreen fields.

The AcceptButtonData object passed to acceptButtonBuilder contains onPressed, isEnabled, currentLocation, and currentAddress fields.

Contributing

Contributions are welcome! If you have suggestions for improvements, feature requests, or bug fixes, feel free to open an issue or submit a pull request on the GitHub repository.

License

This project is licensed under the BSD 3-Clause License. See the LICENSE file for details.

Libraries

location_picker
Location picker module for the neshan_maps_flutter package.
map
Map module for the neshan_maps_flutter package.