flutter_map_smart 1.1.0
flutter_map_smart: ^1.1.0 copied to clipboard
A plug-and-play OpenStreetMap widget with clustering, image markers, user location, and nearby radius support.
import 'package:flutter/material.dart';
import 'package:flutter_map_smart/flutter_map_smart.dart';
void main() {
runApp(const MyApp());
}
class Place {
final String name;
final double lat;
final double lng;
final String? image;
final String description;
final PlaceType type;
const Place({
required this.name,
required this.lat,
required this.lng,
this.image,
required this.description,
required this.type,
});
Color getTypeColor() {
switch (type) {
case PlaceType.restaurant:
return const Color(0xFFEF4444);
case PlaceType.hotel:
return const Color(0xFF3B82F6);
case PlaceType.landmark:
return const Color(0xFFF59E0B);
case PlaceType.hospital:
return const Color(0xFF10B981);
case PlaceType.tech:
return const Color(0xFF8B5CF6);
}
}
}
enum PlaceType { restaurant, hotel, landmark, hospital, tech }
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
brightness: Brightness.light,
fontFamily: 'Inter',
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1E293B), // Slate 800
primary: const Color(0xFF1E293B),
secondary: const Color(0xFF6366F1), // Indigo
surface: Colors.white,
),
scaffoldBackgroundColor: const Color(0xFFF1F5F9),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
surfaceTintColor: Colors.transparent,
elevation: 0,
centerTitle: false,
titleTextStyle: TextStyle(
color: Color(0xFF1E293B),
fontSize: 20,
fontWeight: FontWeight.w700,
),
iconTheme: IconThemeData(color: Color(0xFF1E293B)),
),
cardTheme: CardThemeData(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Colors.white,
),
),
home: const ExampleSelector(),
);
}
}
// ✨ Standard UI Components
class IconBox extends StatelessWidget {
final IconData icon;
final Color color;
final double size;
const IconBox({
super.key,
required this.icon,
required this.color,
this.size = 20,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: size),
);
}
}
// 🎯 Main selector - Modern clean design
class ExampleSelector extends StatelessWidget {
const ExampleSelector({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Smart OSM Map')),
body: ListView(
padding: const EdgeInsets.all(20),
children: const [
_SectionHeader(title: 'Fundamentals'),
SizedBox(height: 12),
_ExampleTile(
title: 'Basic Usage',
subtitle: 'Simple map with markers',
icon: Icons.map_rounded,
example: BasicExample(),
),
SizedBox(height: 12),
_ExampleTile(
title: 'Location & Nearby',
subtitle: 'Proximity based search',
icon: Icons.location_on_rounded,
example: LocationExample(),
),
SizedBox(height: 12),
_ExampleTile(
title: 'Permissions',
subtitle: 'Handling location access',
icon: Icons.security_rounded,
example: PermissionExample(),
),
SizedBox(height: 32),
_SectionHeader(title: 'Advanced'),
SizedBox(height: 12),
_ExampleTile(
title: 'Custom Styling',
subtitle: 'Themes and colors',
icon: Icons.palette_rounded,
example: StylingExample(),
),
SizedBox(height: 12),
_ExampleTile(
title: 'Performance',
subtitle: 'Clustering 1000+ points',
icon: Icons.speed_rounded,
example: PerformanceExample(),
),
SizedBox(height: 12),
_ExampleTile(
title: 'Network Assets',
subtitle: 'Loading remote images',
icon: Icons.cloud_download_rounded,
example: NetworkImageExample(),
),
],
),
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(left: 4),
child: Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF64748B),
letterSpacing: 0.2,
),
),
);
}
}
class _ExampleTile extends StatelessWidget {
final String title;
final String subtitle;
final IconData icon;
final Widget example;
const _ExampleTile({
required this.title,
required this.subtitle,
required this.icon,
required this.example,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: ListTile(
onTap: () =>
Navigator.push(context, MaterialPageRoute(builder: (_) => example)),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: const Color(0xFF1E293B), size: 24),
),
title: Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
subtitle: Text(
subtitle,
style: const TextStyle(fontSize: 13, color: Color(0xFF64748B)),
),
trailing: const Icon(
Icons.chevron_right_rounded,
color: Color(0xFF94A3B8),
),
),
);
}
}
// 📍 Example 1: Basic Usage
class BasicExample extends StatefulWidget {
const BasicExample({super.key});
@override
State<BasicExample> createState() => _BasicExampleState();
}
class _BasicExampleState extends State<BasicExample> {
Place? _selectedPlace;
final places = [
const Place(
name: 'India Gate',
lat: 28.6129,
lng: 77.2295,
image: 'assets/images/india_gate.png',
description: 'War memorial in New Delhi',
type: PlaceType.landmark,
),
const Place(
name: 'Red Fort',
lat: 28.6562,
lng: 77.2410,
image: 'assets/images/red_fort.png',
description: 'Historic Mughal fort',
type: PlaceType.landmark,
),
const Place(
name: 'No Image Marker',
lat: 28.6315,
lng: 77.2167,
image: null,
description: 'Demonstrates default marker',
type: PlaceType.landmark,
),
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Basic Usage'),
backgroundColor: Colors.white,
),
body: Stack(
children: [
FlutterMapSmart.simple(
items: places,
latitude: (p) => p.lat,
longitude: (p) => p.lng,
markerImage: (p) => p.image,
onTap: (place) => setState(() => _selectedPlace = place),
),
if (_selectedPlace != null)
Positioned(
bottom: 24,
left: 20,
right: 20,
child: _PlaceDetailCard(
place: _selectedPlace!,
onClose: () => setState(() => _selectedPlace = null),
),
),
],
),
);
}
}
class _PlaceDetailCard extends StatelessWidget {
final Place place;
final VoidCallback onClose;
const _PlaceDetailCard({required this.place, required this.onClose});
@override
Widget build(BuildContext context) {
final color = place.getTypeColor();
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(Icons.location_on_rounded, color: color, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
place.name,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w700,
color: Color(0xFF1E293B),
),
),
Text(
place.type.name.toUpperCase(),
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w700,
color: color,
letterSpacing: 0.5,
),
),
],
),
),
IconButton(
onPressed: onClose,
icon: const Icon(Icons.close_rounded, size: 20),
style: IconButton.styleFrom(
backgroundColor: const Color(0xFFF1F5F9),
padding: EdgeInsets.zero,
),
),
],
),
const SizedBox(height: 16),
Text(
place.description,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF64748B),
height: 1.5,
),
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF1E293B),
foregroundColor: Colors.white,
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'View Details',
style: TextStyle(fontWeight: FontWeight.w600),
),
),
),
],
),
);
}
}
// 📍 Example 2: Location & Nearby
class LocationExample extends StatefulWidget {
const LocationExample({super.key});
@override
State<LocationExample> createState() => _LocationExampleState();
}
class _LocationExampleState extends State<LocationExample> {
bool showLocation = false;
bool enableNearby = false;
double radiusKm = 10;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Location & Nearby'),
backgroundColor: Colors.white,
),
body: Stack(
children: [
FlutterMapSmart.simple(
items: _generateDelhiPlaces(),
latitude: (p) => p.lat,
longitude: (p) => p.lng,
markerImage: (p) => p.image,
showUserLocation: showLocation,
enableNearby: enableNearby,
nearbyRadiusKm: radiusKm,
radiusColor: const Color(0xFF6366F1).withValues(alpha: 0.12),
),
Positioned(
bottom: 24,
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Location Settings',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF1E293B),
),
),
const SizedBox(height: 16),
_buildToggle(
icon: Icons.my_location_rounded,
title: 'Live Tracking',
value: showLocation,
onChanged: (v) => setState(() => showLocation = v),
activeColor: const Color(0xFF6366F1),
),
const SizedBox(height: 12),
_buildToggle(
icon: Icons.radar_rounded,
title: 'Proximity Filter',
value: enableNearby,
enabled: showLocation,
subtitle: 'Within ${radiusKm.toInt()} km',
onChanged: (v) => setState(() => enableNearby = v),
activeColor: const Color(0xFF6366F1),
),
if (enableNearby) ...[
const SizedBox(height: 20),
_buildRadiusSlider(),
],
],
),
),
),
],
),
);
}
Widget _buildToggle({
required IconData icon,
required String title,
required bool value,
required Function(bool) onChanged,
required Color activeColor,
bool enabled = true,
String? subtitle,
}) {
return Row(
children: [
IconBox(
icon: icon,
color: value ? activeColor : const Color(0xFF94A3B8),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
if (subtitle != null)
Text(
subtitle,
style: const TextStyle(
fontSize: 11,
color: Color(0xFF64748B),
),
),
],
),
),
SizedBox(
height: 32,
child: Switch.adaptive(
value: value,
onChanged: enabled ? onChanged : null,
activeTrackColor: activeColor,
),
),
],
);
}
Widget _buildRadiusSlider() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Search Radius',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF64748B),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF10B981).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${radiusKm.toInt()} km',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w800,
color: Color(0xFF10B981),
),
),
),
],
),
Slider(
value: radiusKm,
min: 1,
max: 50,
divisions: 49,
onChanged: (v) => setState(() => radiusKm = v),
activeColor: const Color(0xFF10B981),
inactiveColor: const Color(0xFFE2E8F0),
),
],
);
}
}
// 📍 Example 3: Permission Handling
class PermissionExample extends StatefulWidget {
const PermissionExample({super.key});
@override
State<PermissionExample> createState() => _PermissionExampleState();
}
class _PermissionExampleState extends State<PermissionExample> {
String? permissionStatus;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Permissions'),
backgroundColor: Colors.white,
),
body: Stack(
children: [
FlutterMapSmart.simple(
items: _generateDelhiPlaces(),
latitude: (p) => p.lat,
longitude: (p) => p.lng,
markerImage: (p) => p.image,
showUserLocation: true,
onLocationPermissionGranted: () {
setState(() => permissionStatus = 'Location permission granted');
},
onLocationPermissionDenied: () {
setState(() => permissionStatus = 'Location permission denied');
},
onLocationPermissionDeniedForever: () {
setState(
() =>
permissionStatus = 'Location permission permanently denied',
);
},
onLocationServiceDisabled: () {
setState(
() => permissionStatus = 'Location services are disabled',
);
},
),
if (permissionStatus != null)
Positioned(
top: 20,
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
const Icon(
Icons.info_outline_rounded,
color: Color(0xFF6366F1),
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
permissionStatus!,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
),
IconButton(
icon: const Icon(Icons.close_rounded, size: 18),
onPressed: () => setState(() => permissionStatus = null),
),
],
),
),
),
Positioned(
bottom: 24,
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Permission Statuses',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Color(0xFF1E293B),
),
),
const SizedBox(height: 16),
_buildPermissionItem(
Icons.check_circle_rounded,
'Granted',
Colors.green,
),
_buildPermissionItem(
Icons.error_outline_rounded,
'Denied',
Colors.orange,
),
_buildPermissionItem(
Icons.cancel_outlined,
'Restricted',
Colors.red,
),
],
),
),
),
],
),
);
}
Widget _buildPermissionItem(IconData icon, String title, Color color) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
children: [
Icon(icon, color: color, size: 18),
const SizedBox(width: 12),
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
],
),
);
}
}
// 📍 Example 4: Custom Styling
class StylingExample extends StatelessWidget {
const StylingExample({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Custom Styling'),
backgroundColor: Colors.white,
),
body: Stack(
children: [
FlutterMapSmart.simple(
items: _generateDelhiPlaces(),
latitude: (p) => p.lat,
longitude: (p) => p.lng,
markerImage: (p) => p.image,
markerSize: 64,
markerBorderColor: (p) => p.getTypeColor(),
clusterColor: const Color(0xFF6366F1),
radiusColor: const Color(0xFF6366F1).withValues(alpha: 0.1),
showUserLocation: true,
enableNearby: true,
nearbyRadiusKm: 15,
),
Positioned(
top: 20,
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Row(
children: [
IconBox(
icon: Icons.palette_rounded,
color: Color(0xFF8B5CF6),
),
SizedBox(width: 12),
Expanded(
child: Text(
'Custom marker aesthetics and cluster themes',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
),
),
),
],
),
),
),
],
),
);
}
}
// 📍 Example 5: Performance Test
class PerformanceExample extends StatelessWidget {
const PerformanceExample({super.key});
@override
Widget build(BuildContext context) {
final places = List.generate(1000, (i) {
final lat = 28.5 + (i % 40) * 0.01;
final lng = 77.1 + (i ~/ 40) * 0.01;
return Place(
name: 'Node $i',
lat: lat,
lng: lng,
image: i % 5 == 0 ? 'https://picsum.photos/100/100?random=$i' : null,
description: 'High-frequency data point $i',
type: PlaceType.tech,
);
});
return Scaffold(
appBar: AppBar(
title: const Text('Performance'),
backgroundColor: Colors.white,
),
body: Stack(
children: [
FlutterMapSmart.simple(
items: places,
latitude: (p) => p.lat,
longitude: (p) => p.lng,
markerImage: (p) => p.image,
useClustering: true,
),
Positioned(
top: 20,
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
const IconBox(
icon: Icons.speed_rounded,
color: Color(0xFFEF4444),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${places.length} Items Rendered',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Color(0xFF1E293B),
),
),
const Text(
'Optimized spatial clustering enabled',
style: TextStyle(
fontSize: 12,
color: Color(0xFF64748B),
),
),
],
),
),
],
),
),
),
],
),
);
}
}
// 📍 Example 6: Network Images
class NetworkImageExample extends StatelessWidget {
const NetworkImageExample({super.key});
@override
Widget build(BuildContext context) {
final places = [
const Place(
name: 'Network Image 01',
lat: 28.6129,
lng: 77.2295,
image: 'https://picsum.photos/400/400?random=10',
description: 'Remote asset resolution',
type: PlaceType.tech,
),
const Place(
name: 'Network Image 02',
lat: 28.6315,
lng: 77.2167,
image: 'https://picsum.photos/400/400?random=20',
description: 'Distributed image caching',
type: PlaceType.tech,
),
const Place(
name: 'Error Handling',
lat: 28.6562,
lng: 77.2410,
image: 'https://invalid-url.com/404.jpg',
description: 'Graceful fallback for missing assets',
type: PlaceType.landmark,
),
];
return Scaffold(
appBar: AppBar(
title: const Text('Network Assets'),
backgroundColor: Colors.white,
),
body: Stack(
children: [
FlutterMapSmart.simple(
items: places,
latitude: (p) => p.lat,
longitude: (p) => p.lng,
markerImage: (p) => p.image,
),
Positioned(
bottom: 24,
left: 20,
right: 20,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: const Row(
children: [
IconBox(
icon: Icons.cloud_done_rounded,
color: Color(0xFF06B6D4),
),
SizedBox(width: 16),
Expanded(
child: Text(
'Network asset synchronization with error recovery',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
height: 1.4,
),
),
),
],
),
),
),
],
),
);
}
}
// Helper function
List<Place> _generateDelhiPlaces() {
return const [
Place(
name: 'India Gate',
lat: 28.6129,
lng: 77.2295,
image: 'assets/images/india_gate.png',
description: 'War memorial',
type: PlaceType.landmark,
),
Place(
name: 'Red Fort',
lat: 28.6562,
lng: 77.2410,
image: 'assets/images/red_fort.png',
description: 'Historic fort',
type: PlaceType.landmark,
),
Place(
name: 'Qutub Minar',
lat: 28.5245,
lng: 77.1855,
image: 'assets/images/qutub_minar.png',
description: 'Ancient minaret',
type: PlaceType.landmark,
),
Place(
name: 'Connaught Place',
lat: 28.6315,
lng: 77.2167,
image: 'assets/images/connaught_place.png',
description: 'Shopping district',
type: PlaceType.landmark,
),
];
}