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.
example/lib/main.dart
import 'dart:async';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';
import 'package:manara_map/manara_map.dart';
// Supplied at build time so no credential is committed:
// flutter run --dart-define=API_KEY=... --dart-define=BASE_URL=https://... \n// --dart-define=TILE_URL=https://.../{z}/{x}/{y}.png
// The defaults are placeholders; the API rejects them, which is the intended
// signal that the defines are missing.
const String _apiKey =
String.fromEnvironment("API_KEY", defaultValue: "API-KEY");
const String _baseUrl =
String.fromEnvironment("BASE_URL", defaultValue: "BASE-URL");
const String _tileUrl =
String.fromEnvironment("TILE_URL", defaultValue: "TILE-URL");
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: 'Manara Map Example',
home: MapExamplePage(),
);
}
}
/// Demonstrates [ManaraMapWidget]: address search, the directions picker,
/// and the current-location button that repurposes itself into "Start
/// Routing" once a route is picked. The show/hide constructor params below
/// mirror [ManaraMapWidget]'s own — see [DemoHomePage], which drives them
/// live from its drawer.
class MapExamplePage extends StatefulWidget {
const MapExamplePage({
super.key,
this.showSearchBar = true,
this.showCurrentLocationButton = true,
this.showCustomLocationButton = true,
this.showAddressText = true,
this.showSearchBarAtBottom = false,
this.searchIconIsLeading = true,
this.showDirectionsButton = true,
});
final bool showSearchBar;
final bool showCurrentLocationButton;
final bool showCustomLocationButton;
final bool showAddressText;
final bool showSearchBarAtBottom;
final bool searchIconIsLeading;
final bool showDirectionsButton;
@override
State<MapExamplePage> createState() => _MapExamplePageState();
}
class _MapExamplePageState extends State<MapExamplePage>
with TickerProviderStateMixin {
late final ManaraMapController _mapController;
StreamSubscription<Position>? _riderPositionSubscription;
/// Whether "Start Routing" is active — while true, real GPS ticks are also
/// fed into [trackingLocation] so the rider marker follows actual device
/// movement instead of sitting still.
bool _isTracking = false;
LocationEntity? _pickLocation;
String get className => runtimeType.toString();
@override
void initState() {
super.initState();
_mapController = ManaraMapController(this);
_publishRealRiderLocation();
}
@override
void dispose() {
_mapController.dispose();
_riderPositionSubscription?.cancel();
super.dispose();
}
/// Publishes the device's real, continuous GPS position through
/// [RiderLocationService] — what would run on a rider's phone.
///
/// This opens its own location stream, which is how a production app should
/// feed the package. [ManaraMapWidget.onLocationChanged] is not a substitute:
/// it only fires when the map center moves. While [_isTracking] is true the
/// same positions also drive the on-map rider marker through
/// `trackingLocation`.
Future<void> _publishRealRiderLocation() async {
if (!await Geolocator.isLocationServiceEnabled()) {
await Geolocator.openLocationSettings();
if (!await Geolocator.isLocationServiceEnabled()) {
return;
}
}
var permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission == LocationPermission.denied ||
permission == LocationPermission.deniedForever) {
await Geolocator.openAppSettings();
permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) return;
}
_riderPositionSubscription = Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.high, distanceFilter: 10),
).listen((position) {
final location = LocationEntity(
latitude: position.latitude, longitude: position.longitude);
// Update on your Service
//_riderLocationService.updateRiderLocation(location);
if (_isTracking) {
setState(() => _pickLocation = location);
_mapController.animateTo(location.latitude, location.longitude, 18.0);
}
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: ManaraMapWidget(
apiKey: _apiKey,
baseUrl: _baseUrl,
tileUrl: _tileUrl,
mapController: _mapController,
showSearchBar: widget.showSearchBar,
showCurrentLocationButton: widget.showCurrentLocationButton,
showCustomLocationButton: widget.showCustomLocationButton,
showAddressText: widget.showAddressText,
showSearchBarAtBottom: widget.showSearchBarAtBottom,
searchIconIsLeading: widget.searchIconIsLeading,
showDirectionsButton: widget.showDirectionsButton,
searchActiveIconColor: Colors.blue,
locationButtonPadding: const EdgeInsetsDirectional.only(bottom: 20),
onLocationChanged: (lat, lng) {
// Fires for map-center changes (initial load, re-center,
// dragging) — not the rider's live GPS position, so it's
// just logged here. See _publishRealRiderLocation for the
// actual RiderLocationService publishing.
if (kDebugMode) {
debugPrint('Location: $lat, $lng');
// _riderLocationService.updateRiderLocation(
// LocationEntity(latitude: lat, longitude: lng));
}
},
onAddressChanged: (lat, lng, address) {
if (kDebugMode) {
debugPrint('Address: $lat, $lng, $address');
}
},
onSearchActiveChanged: (isActive) {
if (kDebugMode) debugPrint('Search active: $isActive');
},
onDirectionsRouteFound: (RouteEntity routeEntity) {
// Seed trackingLocation with the route's own start, so the rider
// marker has a position before the first GPS tick arrives.
// See _publishRealRiderLocation.
if (routeEntity.points.isNotEmpty) {
// _pickLocation = routeEntity.points.first;
// final endLocation = routeEntity.points.last;
// _riderLocationService.updateRiderLocation(
// _pickLocation!, endLocation);
}
},
onRerouting: () {
if (kDebugMode) {
log('🔀 Off-route — rerouting...', name: className);
}
},
onRerouted: (RouteEntity routeEntity) {
if (kDebugMode) {
log('✅ Rerouted with ${routeEntity.points.length} points',
name: className);
}
if (routeEntity.points.isNotEmpty) {
// _pickLocation = routeEntity.points.first;
// final endLocation = routeEntity.points.last;
// _riderLocationService.updateRiderLocation(
// _pickLocation!, endLocation);
}
},
trackingLocation: _pickLocation,
onStartRouting: () {
if (kDebugMode) log('Route started', name: className);
_isTracking = true;
if (_pickLocation != null) {
_mapController.animateTo(
_pickLocation!.latitude, _pickLocation!.longitude, 18.0);
}
},
onTrackingCompleted: () {
if (kDebugMode) log('Rider arrived', name: className);
_isTracking = false;
},
),
),
],
);
}
}
/// Demonstrates [ManaraRouteMapWidget]: a fixed pickup/dropoff pair, a
/// "Find Route" button that fetches the route between them, and a
/// "Start Route" button once one is found.
class RouteExamplePage extends StatefulWidget {
const RouteExamplePage({super.key});
@override
State<RouteExamplePage> createState() => _RouteExamplePageState();
}
class _RouteExamplePageState extends State<RouteExamplePage>
with TickerProviderStateMixin {
late final ManaraMapController _mapController;
StreamSubscription<(LocationEntity?, LocationEntity?)>?
_riderLocationSubscription;
late LocationEntity _pickup;
/// The consumer's own current device location — where the rider is headed.
/// Fetched once (not streamed like [_pickup]/the rider's position): unlike
/// the rider, the consumer isn't expected to be moving while waiting for
/// the delivery. Null until resolved.
LocationEntity? _dropoff;
String get className => runtimeType.toString();
@override
void initState() {
super.initState();
_pickup = LocationEntity(
latitude: 31.467192334473534,
longitude: 74.29420709609987,
);
_mapController = ManaraMapController(this);
// The customer's side of the delivery: follow the rider's live position,
// published by MapExamplePage through RiderLocationService.
// _riderLocationSubscription =
// _riderLocationService.streamRiderLocation().listen((data) {
// if (kDebugMode) log("Location ---> $data", name: runtimeType.toString());
// final (location, endLocation) = data;
// if (location != null) setState(() => _pickup = location);
// if (_dropoff != endLocation) setState(() => _dropoff = endLocation);
// });
}
@override
void dispose() {
_mapController.dispose();
_riderLocationSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final dropoff = _dropoff;
return dropoff == null
? const Center(child: CircularProgressIndicator())
: ManaraRouteMapWidget(
apiKey: _apiKey,
baseUrl: _baseUrl,
tileUrl: _tileUrl,
mapController: _mapController,
startLocation: _pickup,
endLocation: dropoff,
onRouteFound: (route) {
if (kDebugMode) {
log('Route found with ${route.points.length} points',
name: className);
}
},
onRouteError: (message) {
if (kDebugMode) {
log('Route error: $message', name: className);
}
},
onRerouting: () {
if (kDebugMode) {
log('🔀 Off-route — rerouting...', name: className);
}
},
onRerouted: (route) {
if (kDebugMode) {
log('✅ Rerouted with ${route.points.length} points',
name: className);
}
},
);
}
}