neshan_maps_flutter 1.1.0
neshan_maps_flutter: ^1.1.0 copied to clipboard
A Flutter SDK for Neshan Maps. Provides an interactive map widget and a ready-to-use location picker with reverse-geocoding and search.
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 |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
Table of Contents #
- Getting API Keys
- Installation
- Screenshots
- Quick Start
- Location Permission Setup
- NeshanMap — Full Reference
- NeshanLocationPicker — Full Reference
- Contributing
- License
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: trueinNeshanMapConfig(the default).
If you setshowCurrentLocationButton: 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.readybefore 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
searchApiKeyis provided) that opens a full-screen search UI. - A confirm button that calls
onLocationAcceptedwith the finalLatLngand 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.




