apple_map_snapshotter 1.0.0
apple_map_snapshotter: ^1.0.0 copied to clipboard
Generate static Apple Maps snapshots on iOS using MapKit's MKMapSnapshotter. Perfect for displaying map previews without loading a full interactive map.
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:apple_map_snapshotter/apple_map_snapshotter.dart';
void main() {
runApp(const AppleMapSnapshotterExampleApp());
}
class AppleMapSnapshotterExampleApp extends StatelessWidget {
const AppleMapSnapshotterExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Apple Map Snapshotter Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const ExampleScreen(),
);
}
}
class ExampleScreen extends StatefulWidget {
const ExampleScreen({super.key});
@override
State<ExampleScreen> createState() => _ExampleScreenState();
}
class _ExampleScreenState extends State<ExampleScreen> {
Uint8List? _snapshotBytes;
bool _isLoading = false;
String? _error;
AppleMapSnapshotTheme _selectedTheme = AppleMapSnapshotTheme.auto;
double _zoomLevel = 13;
// Example locations
static const _locations = [
_Location('San Francisco', 37.7749, -122.4194),
_Location('New York', 40.7128, -74.0060),
_Location('London', 51.5074, -0.1278),
_Location('Paris', 48.8566, 2.3522),
_Location('Tokyo', 35.6762, 139.6503),
];
_Location _selectedLocation = _locations[0];
@override
void initState() {
super.initState();
_loadSnapshot();
}
Future<void> _loadSnapshot() async {
if (!Platform.isIOS) {
setState(() {
_error = 'This plugin only works on iOS';
_isLoading = false;
});
return;
}
setState(() {
_isLoading = true;
_error = null;
});
try {
final bytes = await AppleMapSnapshotter.getSnapshot(
latitude: _selectedLocation.latitude,
longitude: _selectedLocation.longitude,
width: 350,
height: 250,
zoomLevel: _zoomLevel,
theme: _selectedTheme,
);
setState(() {
_snapshotBytes = bytes;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Map Snapshotter'),
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Location selector
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Location',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
DropdownButtonFormField<_Location>(
value: _selectedLocation,
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
items: _locations.map((loc) {
return DropdownMenuItem(
value: loc,
child: Text(loc.name),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() => _selectedLocation = value);
}
},
),
const SizedBox(height: 8),
Text(
'Lat: ${_selectedLocation.latitude.toStringAsFixed(4)}, '
'Lng: ${_selectedLocation.longitude.toStringAsFixed(4)}',
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
),
),
),
const SizedBox(height: 16),
// Theme selector
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Theme',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
SegmentedButton<AppleMapSnapshotTheme>(
segments: const [
ButtonSegment(
value: AppleMapSnapshotTheme.auto,
label: Text('Auto'),
),
ButtonSegment(
value: AppleMapSnapshotTheme.light,
label: Text('Light'),
),
ButtonSegment(
value: AppleMapSnapshotTheme.dark,
label: Text('Dark'),
),
],
selected: {_selectedTheme},
onSelectionChanged: (Set<AppleMapSnapshotTheme> selected) {
setState(() => _selectedTheme = selected.first);
},
),
],
),
),
),
const SizedBox(height: 16),
// Zoom slider
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Zoom Level',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
Text(
_zoomLevel.toStringAsFixed(1),
style: TextStyle(
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
],
),
Slider(
value: _zoomLevel,
min: 0,
max: 20,
divisions: 40,
onChanged: (value) {
setState(() => _zoomLevel = value);
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('World', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
Text('Street', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
],
),
],
),
),
),
const SizedBox(height: 16),
// Generate button
FilledButton.icon(
onPressed: _isLoading ? null : _loadSnapshot,
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.map),
label: Text(_isLoading ? 'Generating...' : 'Generate Snapshot'),
),
const SizedBox(height: 24),
// Snapshot display
if (_error != null)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.red[200]!),
),
child: Row(
children: [
Icon(Icons.error_outline, color: Colors.red[700]),
const SizedBox(width: 12),
Expanded(
child: Text(
_error!,
style: TextStyle(color: Colors.red[700]),
),
),
],
),
)
else if (_snapshotBytes != null)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Generated Snapshot',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.memory(
_snapshotBytes!,
width: double.infinity,
fit: BoxFit.cover,
),
),
const SizedBox(height: 8),
Text(
'Size: ${(_snapshotBytes!.lengthInBytes / 1024).toStringAsFixed(1)} KB',
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
],
)
else if (!_isLoading)
Container(
height: 200,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text('Tap "Generate Snapshot" to create a map'),
),
),
],
),
),
);
}
}
class _Location {
final String name;
final double latitude;
final double longitude;
const _Location(this.name, this.latitude, this.longitude);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _Location &&
runtimeType == other.runtimeType &&
name == other.name;
@override
int get hashCode => name.hashCode;
}