manara_map 1.0.0
manara_map: ^1.0.0 copied to clipboard
A Flutter package for OpenStreetMap-based maps with address search, location services, turn-by-turn directions with live rider tracking, and customizable UI.
A drop-in Flutter widget for the Manara Maps service: an OpenStreetMap-based map with address search, device location, turn-by-turn directions, and live rider tracking β all wired up behind a single API key.
Features #
- π Current location β resolves the device's location on load, with automatic reverse geocoding of the map center as the user pans (debounced).
- π Address search β type-ahead search-as-you-type with debounce, loading state, and a
results list, or replace the field entirely with your own via
searchFieldBuilder. - π§ Directions β a source/destination picker, route drawing, and turn-by-turn rider tracking with automatic rerouting when the rider drifts off the route.
- π΅ Live tracking widget β
ManaraRouteMapWidget, a watch-only map for following a delivery in progress between a fixed pickup and drop-off. - π¨ Customizable UI β swap markers, buttons, search/directions fields, colors, and route line styling without forking the widget.
- πΊοΈ Built on
flutter_map(OpenStreetMap tiles), so it never depends on Google Maps or an API key from Google.
Getting started #
Important β SDK requirements
This package requires Flutter >=3.22.0 and Dart ^3.8.0 (see
environment:inpubspec.yaml);flutter pub getrefuses to resolve it on an older Flutter install.
manara_mapitself ships no native Android/iOS code, so it doesn't declare acompileSdkorminSdkof its own. However, its location dependency,geolocator(viageolocator_android), pulls inandroidx.core:core:1.13.0, which needs AndroidcompileSdk34 or higher to build. Flutter's default project template already setscompileSdk/minSdkfromflutter.compileSdkVersion/flutter.minSdkVersionin your app'sandroid/app/build.gradle, and any Flutter SDK β₯3.22 defaults those to 34+, so most consumers won't need to change anything β but if your app'sbuild.gradlepinscompileSdkto a literal, older number (rather thanflutter.compileSdkVersion), raise it to at least 34 or the Android build will fail.
2. Get an API key #
ManaraMapWidget and ManaraRouteMapWidget talk to the Manara Maps HTTP API for address search,
reverse geocoding, and routing. You'll need an API key from Manara Maps to use these features β
map tiles themselves are unauthenticated.
Your API key is sent as an sm-api-key request header β never in a URL β and is never logged.
Keep it out of source control: supply it at build time with --dart-define, as the example app
does, or fetch it from your own backend at runtime.
Alongside your API key, every API request automatically carries your app's package id (the Android
applicationId / iOS bundle identifier) in an sm-app-id header, so Manara Maps can attribute
traffic to your app. This needs no configuration on your side, and it is best-effort: if the id
can't be read on a given platform, requests still go out without the header. Map tile requests are
unaffected.
baseUrlmust usehttps. Requests carry your API key in a header and the device's coordinates in the query string, so a plain-httpbase URL would put both on the wire unencrypted. Debug builds print a warning if it does not. A trailing slash is fine β it is stripped for you.
tileUrlis required too, and is separate frombaseUrl: it is the{z}/{x}/{y}template your raster tiles are fetched from, for examplehttps://your-tile-host.example.com/{z}/{x}/{y}.png. Tile requests carry no API key.Both values may arrive as
nullor empty β a remote-config lookup that has not resolved yet, say β in which case the widget renders its plain error overlay instead of building a map.
3. Configure platform permissions #
Both widgets use geolocator to resolve the device's
location, so the usual location-permission setup for that plugin applies:
Android β in android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
iOS β in ios/Runner/Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs location access</string>
If you need the rider's location to keep updating while tracking runs in the background, add
NSLocationAlwaysAndWhenInUseUsageDescription as well and request the corresponding permission
from geolocator yourself.
Usage #
Basic map with search and location #
ManaraMapWidget needs an apiKey and a ManaraMapController, which requires a TickerProvider
(typically your State's TickerProviderStateMixin):
import 'package:flutter/material.dart';
import 'package:manara_map/manara_map.dart';
class MapPage extends StatefulWidget {
const MapPage({super.key});
@override
State<MapPage> createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> with TickerProviderStateMixin {
late final ManaraMapController _mapController;
@override
void initState() {
super.initState();
_mapController = ManaraMapController(this);
}
@override
void dispose() {
_mapController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ManaraMapWidget(
apiKey: 'your-api-key',
baseUrl: 'https://your-manara-maps-api.example.com/api',
tileUrl: 'https://your-tile-host.example.com/{z}/{x}/{y}.png',
mapController: _mapController,
onLocationChanged: (lat, lng) => debugPrint('Location: $lat, $lng'),
onAddressChanged: (lat, lng, address) => debugPrint('Address: $address'),
);
}
}
By default this renders a map centered on the device's current location, a search bar, a "locate me" button, and the resolved address underneath the map. Pan the map to reverse-geocode a new center; tap a search result to jump to it.
Turn-by-turn directions #
showDirectionsButton defaults to true, which adds a directions toggle next to the search bar.
Tapping it swaps in a source/destination picker; once both points are chosen, the widget fetches
and draws the route, and the current-location button turns into a "Start Routing" button:
ManaraMapWidget(
apiKey: 'your-api-key',
baseUrl: 'https://your-manara-maps-api.example.com/api',
tileUrl: 'https://your-tile-host.example.com/{z}/{x}/{y}.png',
mapController: _mapController,
onDirectionsRouteFound: (route) => debugPrint('Route with ${route.points.length} points'),
onDirectionsRouteError: (message) => debugPrint('Directions failed: $message'),
onStartRouting: () {
// The user tapped "Start Routing". Begin feeding live GPS updates into
// trackingLocation below (your own location stream, a background
// service, or test data).
},
trackingLocation: _riderLocation,
onTrackingCompleted: () => debugPrint('Rider arrived'),
onRerouting: () => debugPrint('Off-route β reroutingβ¦'),
onRerouted: (route) => debugPrint('Rerouted with ${route.points.length} points'),
)
While tracking is active, feed the rider's current position into trackingLocation by rebuilding
the widget with a new LocationEntity (e.g. from a Geolocator.getPositionStream() listener). The
widget automatically triggers a reroute if the rider drifts more than offRouteThresholdMeters
(default 50.0) from the current route, and fires onTrackingCompleted once the rider reaches
the destination.
Watch-only tracking: ManaraRouteMapWidget #
Use ManaraRouteMapWidget when you already know the fixed pickup and drop-off β for example, a
customer watching their delivery in progress. It has no buttons or manual interaction: it finds
the route and starts tracking as soon as it's built, and re-fits the camera whenever the points
change.
ManaraRouteMapWidget(
apiKey: 'your-api-key',
baseUrl: 'https://your-manara-maps-api.example.com/api',
tileUrl: 'https://your-tile-host.example.com/{z}/{x}/{y}.png',
mapController: _mapController,
startLocation: pickupLocation,
endLocation: dropoffLocation,
onRouteFound: (route) => debugPrint('Route found'),
onRouteError: (message) => debugPrint('Route error: $message'),
onStartRoute: () => debugPrint('Tracking started'),
onRouteEnd: () => debugPrint('Rider arrived'),
onRerouting: () => debugPrint('Off-route β reroutingβ¦'),
onRerouted: (route) => debugPrint('Rerouted'),
)
Rebuild with a new startLocation to feed live rider updates in β the marker glides smoothly to
each new position rather than snapping. Rebuilding with a new endLocation (or a changed
startLocation before tracking has begun) resets and re-finds the route.
Custom search field #
Replace the default search field with any widget via searchFieldBuilder. The package still owns
debounce, loading state, and rendering of search results β only the field itself is swapped out:
ManaraMapWidget(
apiKey: 'your-api-key',
baseUrl: 'https://your-manara-maps-api.example.com/api',
tileUrl: 'https://your-tile-host.example.com/{z}/{x}/{y}.png',
mapController: _mapController,
searchFieldBuilder: (context, search) {
return TextField(
controller: search.controller,
focusNode: search.focusNode,
onChanged: search.onChanged,
onTap: search.onTap,
decoration: InputDecoration(
hintText: 'Search for a location',
suffixIcon: search.query.isEmpty
? IconButton(icon: const Icon(Icons.search), onPressed: search.onSearch)
: IconButton(icon: const Icon(Icons.clear), onPressed: search.onClear),
),
);
},
)
search.controller and search.focusNode are owned and disposed by the package β reuse them,
don't create or dispose your own. Use search.isLoading and search.isFocused to render custom
state.
directionsFieldBuilder follows the same contract for the directions picker's source/destination
field pair (DirectionsFieldContext exposes a controller/focus node/query/loading flag for each
of source and destination, plus onClose to exit the picker).
Custom markers and buttons #
customMarkerWidgetreplaces the center pin (or every marker inmarkers, when supplied).directionsMarkerBuilder/ManaraRouteMapWidget.markerBuilderbuild a custom start/end route marker, called once per point with aRouteMarkerType.directionsRiderMarkerBuilder/ManaraRouteMapWidget.riderMarkerBuilderbuild a custom marker for the rider's live position while tracking, given the current heading in degrees (0Β° = North, clockwise) to rotate against.directionsButtonBuilderbuilds a custom directions toggle button, givenonPressedand whether the pickerisActive.
Displaying a fixed set of markers #
Pass markers to show a static list of pins instead of the draggable center marker β useful for
e.g. showing nearby saved addresses:
ManaraMapWidget(
apiKey: 'your-api-key',
baseUrl: 'https://your-manara-maps-api.example.com/api',
tileUrl: 'https://your-tile-host.example.com/{z}/{x}/{y}.png',
mapController: _mapController,
markers: [
LocationEntity(latitude: 24.8607, longitude: 67.0011),
LocationEntity(latitude: 24.9056, longitude: 67.0822),
],
)
Programmatic map control #
ManaraMapController wraps flutter_map_animations
for panning/zooming outside of user gestures:
_mapController.moveTo(24.8607, 67.0011, 16.0); // jump immediately
_mapController.animateTo(24.8607, 67.0011, 16.0); // animate over ~1s
Handling errors #
Failures surface as typed exceptions exported from manara_map:
ApiExceptionβ the Manara Maps API call failed;statusCodeis populated when available (a401/403typically means an invalid or expired API key).ManaraMapWidgetshows anApiErrorOverlayautomatically when address lookup fails this way.NetworkExceptionβ no network connectivity.LocationExceptionβ device location services are disabled.PermissionExceptionβ location permission was denied.
Search failures are reported inline in the search results list rather than through the
address-lookup error overlay β a broken search shouldn't blank out the whole map. Directions
failures are reported via onDirectionsRouteError / onRouteError.
Widget reference #
ManaraMapWidget #
| Parameter | Type | Description |
|---|---|---|
apiKey |
String |
Required. Manara Maps API key, sent as the sm-api-key header. |
baseUrl |
String? |
Required. Base URL of your Manara Maps environment. Must use https. |
tileUrl |
String? |
Required. {z}/{x}/{y} template the map's raster tiles are fetched from. |
mapController |
ManaraMapController |
Required. |
initialLocation |
LocationEntity? |
Initial center, e.g. when editing an existing address. |
defaultLocation |
LocationEntity? |
Fallback center when device location is unavailable. |
showSearchBar, showCurrentLocationButton, showAddressText, showDirectionsButton |
bool |
Toggle default UI pieces. All default to true. |
showCustomLocationButton |
bool |
Use the alternate "Locate me" button style. Default false. |
customMarkerWidget |
Widget? |
Replaces the center pin (or every pin in markers). |
markers |
List<LocationEntity>? |
Static pins instead of the draggable center marker. |
initialZoom |
double? |
Default 16.0. |
searchFieldBuilder |
MapSearchFieldBuilder? |
Custom search field; see Custom search field. |
directionsFieldBuilder |
DirectionsFieldBuilder? |
Custom source/destination fields. |
directionsButtonBuilder |
DirectionsButtonBuilder? |
Custom directions toggle button. |
directionsMarkerBuilder |
RouteMarkerBuilder? |
Custom start/end route marker. |
directionsRiderMarkerBuilder |
RiderMarkerBuilder? |
Custom live-rider marker while tracking. |
trackingLocation |
LocationEntity? |
Feeds the rider's live position in while tracking is active. |
offRouteThresholdMeters |
double? |
Drift distance (meters) that triggers a reroute. Default 50.0. |
onLocationChanged |
OnLocationChanged? |
(lat, lng) β fires on any map-center change. |
onAddressChanged |
OnAddressChanged? |
(lat, lng, address) β fires once the address resolves. |
onSearchActiveChanged |
OnSearchActiveChanged? |
Search results visibility changed. |
onDirectionsRouteFound |
OnDirectionsRouteFound? |
A route was found for the picked source/destination. |
onDirectionsRouteError |
OnDirectionsRouteError? |
The directions request failed. |
onDirectionsModeChanged |
ValueChanged<bool>? |
The directions picker opened/closed. |
onStartRouting |
VoidCallback? |
The "Start Routing" button was tapped. |
onTrackingCompleted |
VoidCallback? |
The rider reached the destination. |
onRerouting / onRerouted |
VoidCallback? / OnDirectionsRouteFound? |
A reroute started / succeeded. |
Plus a large set of search*/directions* styling parameters (colors, hint text, fill/border
colors, line color/width) β see the dartdoc on ManaraMapWidget for the full list.
ManaraRouteMapWidget #
| Parameter | Type | Description |
|---|---|---|
apiKey |
String |
Required. Manara Maps API key, sent as the sm-api-key header. |
baseUrl |
String? |
Required. Base URL of your Manara Maps environment. Must use https. |
tileUrl |
String? |
Required. {z}/{x}/{y} template the map's raster tiles are fetched from. |
mapController |
ManaraMapController |
Required. |
startLocation / endLocation |
LocationEntity |
Required. Fixed pickup/drop-off; rebuild with a new startLocation to feed live rider updates. |
markerBuilder |
RouteMarkerBuilder? |
Custom start/end marker. |
riderMarkerBuilder |
RiderMarkerBuilder? |
Custom live-rider marker. |
routeLineColor / routeLineWidth |
Color? / double? |
Defaults to the package accent color / 6.0. |
offRouteThresholdMeters |
double? |
Default 50.0. |
onRouteFound / onRouteError |
OnRouteFound? / OnRouteError? |
|
onStartRoute / onRouteEnd |
VoidCallback? |
Tracking started / rider arrived. |
onRerouting / onRerouted |
VoidCallback? / OnRouteFound? |
Entities and exceptions #
LocationEntity({ latitude, longitude })SearchResultEntity({ displayName, latitude, longitude })RouteEntity({ points: List<LocationEntity> })ApiException,NetworkException,LocationException,PermissionExceptionβ see Handling errors.
Additional information #
The example/ app in this repository demonstrates both widgets, including a real GPS-driven
tracking flow. File issues or questions with the Manara Maps team.