sharemap_maplib_flutter 0.0.3
sharemap_maplib_flutter: ^0.0.3 copied to clipboard
A MapLibre-based map widget for Flutter integrating ShareMap APIs, supporting dynamic POI layer toggling, HMAC-signed requests, and custom style loading.
example/lib/main.dart
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:sharemap_maplib_flutter/sharemap_maplib_flutter.dart';
import 'package:pointer_interceptor/pointer_interceptor.dart';
void main() {
runApp(const ShareMapDemoApp());
}
class ShareMapDemoApp extends StatelessWidget {
const ShareMapDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'ShareMap MapLibre Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
brightness: Brightness.dark,
primaryColor: const Color(0xFF0284C7), // Sky blue
scaffoldBackgroundColor: const Color(0xFF0F172A), // Dark slate
cardColor: const Color(0xFF1E293B),
fontFamily: 'Inter',
colorScheme: const ColorScheme.dark(
primary: Color(0xFF38BDF8),
secondary: Color(0xFF34D399), // Emerald accent
surface: Color(0xFF1E293B),
),
),
home: const MapDemoScreen(),
);
}
}
class MapDemoScreen extends StatefulWidget {
const MapDemoScreen({super.key});
@override
State<MapDemoScreen> createState() => _MapDemoScreenState();
}
class _MapDemoScreenState extends State<MapDemoScreen> {
final _apiKeyController = TextEditingController();
final _secretKeyController = TextEditingController();
String _apiKey = '';
String _secretKey = '';
bool _isMapLaunched = false;
ShareMapStyle _selectedStyle = ShareMapStyle.sharemap;
MapLibreMapController? _mapController;
LatLng? _clickedLocation;
String _mapStatus = 'Ready';
bool _showSettings = true;
final List<String> _activeLayerCodes = ['vn-industrial-zones']; // Dynamic list of active layer codes
// Sensible start coordinate (Hanoi, Vietnam)
static const _initialPosition = CameraPosition(
target: LatLng(21.0285, 105.8542),
zoom: 12.0,
);
@override
void dispose() {
_apiKeyController.dispose();
_secretKeyController.dispose();
super.dispose();
}
void _handleMapCreated(MapLibreMapController controller) {
setState(() {
_mapController = controller;
_mapStatus = 'Map Initialized';
});
}
void _handleStyleLoaded() {
setState(() {
_mapStatus = 'Style Fully Loaded';
});
}
void _handleMapClick(Point<double> point, LatLng latLng) {
setState(() {
_clickedLocation = latLng;
_mapStatus =
'Clicked at: ${latLng.latitude.toStringAsFixed(4)}, ${latLng.longitude.toStringAsFixed(4)}';
});
// We can also animate the camera to the clicked location to showcase controller capability
_mapController?.animateCamera(CameraUpdate.newLatLng(latLng));
}
void _zoomIn() {
_mapController?.animateCamera(CameraUpdate.zoomIn());
}
void _zoomOut() {
_mapController?.animateCamera(CameraUpdate.zoomOut());
}
void _centerTo(LatLng target) {
_mapController?.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(target: target, zoom: 14.0),
),
);
setState(() {
_mapStatus = 'Centered camera';
});
}
@override
Widget build(BuildContext context) {
final isDesktop = MediaQuery.of(context).size.width > 800;
return Scaffold(
body: Stack(
children: [
// 1. Map View or Setup Screen
Positioned.fill(
child: !_isMapLaunched
? _buildSetupView()
: ShareMapLibre(
mode: ShareMapMode.dev,
xApiKey: _apiKey.isEmpty ? null : _apiKey,
secretKey: _secretKey.isEmpty ? null : _secretKey,
mapStyle: _selectedStyle,
onMapCreated: _handleMapCreated,
initialCameraPosition: _initialPosition,
onStyleLoadedCallback: _handleStyleLoaded,
onMapClick: _handleMapClick,
myLocationEnabled: false,
layerCodes: _activeLayerCodes.isEmpty ? null : _activeLayerCodes,
),
),
// 2. Custom Sleek Controller Overlay (only shown if map is active)
if (_isMapLaunched && _mapController != null) ...[
_buildSleekControlsOverlay(),
_buildSideControlPanel(isDesktop),
_buildStatusToast(),
],
// 3. Setup trigger floating button if hidden on mobile
if (_isMapLaunched && !isDesktop)
Positioned(
top: 16,
left: 16,
child: SafeArea(
child: FloatingActionButton.small(
backgroundColor: const Color(0xFF1E293B).withValues(alpha: 0.9),
foregroundColor: Colors.white,
onPressed: () {
setState(() {
_showSettings = !_showSettings;
});
},
child: Icon(
_showSettings ? Icons.close_rounded : Icons.menu_rounded,
),
),
),
),
],
),
);
}
Widget _buildSetupView() {
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Color(0xFF0F172A),
Color(0xFF1E1B4B), // Indigo hints
],
),
),
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Container(
constraints: const BoxConstraints(maxWidth: 460),
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: const Color(0xFF1E293B).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: const Color(0xFF334155), width: 1.5),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.4),
blurRadius: 30,
offset: const Offset(0, 15),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header Logo/Icon
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.map_rounded, color: Color(0xFF38BDF8), size: 40),
SizedBox(width: 12),
Text(
'ShareMap Live',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w800,
letterSpacing: 0.5,
color: Colors.white,
),
),
],
),
const SizedBox(height: 16),
const Text(
'Enter your ShareMap API Key to access premium vector styles and map tiles compiled natively in MapLibre.',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF94A3B8),
fontSize: 14,
height: 1.5,
),
),
const SizedBox(height: 32),
// API Key Field
const Text(
'API KEY (x-api-key)',
style: TextStyle(
color: Color(0xFF38BDF8),
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 8),
TextField(
controller: _apiKeyController,
obscureText: true,
style: const TextStyle(color: Colors.white, letterSpacing: 2),
decoration: InputDecoration(
hintText: 'Enter x-api-key here...',
hintStyle: const TextStyle(
color: Color(0xFF64748B),
letterSpacing: 0.5,
),
filled: true,
fillColor: const Color(0xFF0F172A),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF334155)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFF38BDF8),
width: 1.5,
),
),
prefixIcon: const Icon(
Icons.vpn_key_rounded,
color: Color(0xFF38BDF8),
),
),
),
const SizedBox(height: 16),
// Secret Key Field
const Text(
'SECRET KEY (Optional for HMAC)',
style: TextStyle(
color: Color(0xFF38BDF8),
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 8),
TextField(
controller: _secretKeyController,
obscureText: true,
style: const TextStyle(color: Colors.white, letterSpacing: 2),
decoration: InputDecoration(
hintText: 'Enter secret key here...',
hintStyle: const TextStyle(
color: Color(0xFF64748B),
letterSpacing: 0.5,
),
filled: true,
fillColor: const Color(0xFF0F172A),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Color(0xFF334155)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFF38BDF8),
width: 1.5,
),
),
prefixIcon: const Icon(
Icons.security_rounded,
color: Color(0xFF38BDF8),
),
),
),
const SizedBox(height: 24),
// Style Selection
const Text(
'INITIAL MAP THEME',
style: TextStyle(
color: Color(0xFF38BDF8),
fontSize: 12,
fontWeight: FontWeight.bold,
letterSpacing: 1,
),
),
const SizedBox(height: 10),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF334155)),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<ShareMapStyle>(
value: _selectedStyle,
dropdownColor: const Color(0xFF0F172A),
isExpanded: true,
style: const TextStyle(color: Colors.white, fontSize: 14),
items: ShareMapStyle.values.map((style) {
return DropdownMenuItem(
value: style,
child: Text(
style.name.toUpperCase(),
style: const TextStyle(fontWeight: FontWeight.bold),
),
);
}).toList(),
onChanged: (val) {
if (val != null) {
setState(() {
_selectedStyle = val;
});
}
},
),
),
),
const SizedBox(height: 36),
// Launch Button
SizedBox(
height: 52,
child: ElevatedButton(
onPressed: () {
final apiKeyVal = _apiKeyController.text.trim();
final secretKeyVal = _secretKeyController.text.trim();
setState(() {
_apiKey = apiKeyVal;
_secretKey = secretKeyVal;
_isMapLaunched = true;
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0284C7),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Launch Map View',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
SizedBox(width: 8),
Icon(Icons.arrow_forward_rounded, size: 20),
],
),
),
),
],
),
),
),
),
);
}
Widget _buildSleekControlsOverlay() {
return Positioned(
bottom: 24,
right: 24,
child: PointerInterceptor(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Zoom panel
Container(
decoration: BoxDecoration(
color: const Color(0xFF1E293B).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF334155)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.add_rounded, color: Colors.white),
onPressed: _zoomIn,
tooltip: 'Zoom In',
),
Divider(
color: const Color(0xFF334155),
height: 1,
thickness: 1,
),
IconButton(
icon: const Icon(Icons.remove_rounded, color: Colors.white),
onPressed: _zoomOut,
tooltip: 'Zoom Out',
),
],
),
),
const SizedBox(height: 12),
// Clear Cache action button
FloatingActionButton.small(
backgroundColor: const Color(0xFFF43F5E), // Rose red
foregroundColor: Colors.white,
onPressed: () {
ShareMapLibre.clearCache();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('ShareMap style cache cleared!'),
backgroundColor: Color(0xFF10B981), // Emerald green
),
);
},
tooltip: 'Clear Cache',
child: const Icon(Icons.delete_sweep_rounded),
),
],
),
),
);
}
Widget _buildSideControlPanel(bool isDesktop) {
if (!isDesktop && !_showSettings) return const SizedBox.shrink();
return Positioned(
top: isDesktop ? 24 : 76,
left: 24,
bottom: isDesktop ? 24 : 76,
child: PointerInterceptor(
child: Container(
width: 320,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF1E293B).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: const Color(0xFF334155)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
const Icon(Icons.settings_rounded, color: Color(0xFF38BDF8)),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Map Controls',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
if (!isDesktop)
IconButton(
icon: const Icon(
Icons.close_rounded,
color: Color(0xFF94A3B8),
),
onPressed: () => setState(() => _showSettings = false),
),
],
),
const SizedBox(height: 16),
const Divider(color: Color(0xFF334155)),
const SizedBox(height: 12),
// Active Key info
const Text(
'ACTIVE API KEY',
style: TextStyle(
color: Color(0xFF64748B),
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const SizedBox(height: 4),
Row(
children: [
const Icon(
Icons.key_rounded,
size: 14,
color: Color(0xFF34D399),
),
const SizedBox(width: 6),
Expanded(
child: Text(
_apiKey.isEmpty
? 'NONE (Optional)'
: (_apiKey.length > 12
? '${_apiKey.substring(0, 12)}...'
: _apiKey),
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontFamily: 'Courier',
),
),
),
TextButton(
onPressed: () {
setState(() {
_isMapLaunched = false;
_mapController = null;
});
},
style: TextButton.styleFrom(
padding: EdgeInsets.zero,
minimumSize: const Size(50, 30),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
'Change',
style: TextStyle(color: Color(0xFFF43F5E), fontSize: 12),
),
),
],
),
const SizedBox(height: 8),
const Text(
'ACTIVE SECRET KEY',
style: TextStyle(
color: Color(0xFF64748B),
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const SizedBox(height: 4),
Row(
children: [
const Icon(
Icons.security_rounded,
size: 14,
color: Color(0xFF38BDF8),
),
const SizedBox(width: 6),
Expanded(
child: Text(
_secretKey.isEmpty
? 'NONE (Optional)'
: (_secretKey.length > 12
? '${_secretKey.substring(0, 12)}...'
: _secretKey),
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontFamily: 'Courier',
),
),
),
],
),
const SizedBox(height: 24),
// Dynamic Style selection
const Text(
'SWITCH STYLE THEME',
style: TextStyle(
color: Color(0xFF64748B),
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
...ShareMapStyle.values.map((style) {
final isSelected = _selectedStyle == style;
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: InkWell(
onTap: () {
setState(() {
_selectedStyle = style;
});
},
borderRadius: BorderRadius.circular(10),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF0284C7).withValues(alpha: 0.2)
: Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected
? const Color(0xFF0284C7)
: const Color(0xFF334155),
width: 1.5,
),
),
child: Row(
children: [
Icon(
isSelected
? Icons.radio_button_checked_rounded
: Icons.radio_button_off_rounded,
color: isSelected
? const Color(0xFF38BDF8)
: const Color(0xFF64748B),
size: 18,
),
const SizedBox(width: 12),
Text(
style.name.toUpperCase(),
style: TextStyle(
color: isSelected
? Colors.white
: const Color(0xFF94A3B8),
fontWeight: isSelected
? FontWeight.bold
: FontWeight.normal,
fontSize: 14,
),
),
],
),
),
),
);
}),
const SizedBox(height: 24),
// Layer Codes Control
const Text(
'POI LAYER CODES',
style: TextStyle(
color: Color(0xFF64748B),
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
// Preset vn_industrial_zones switch
SwitchListTile(
value: _activeLayerCodes.contains('vn-industrial-zones'),
title: const Text(
'Industrial Zones',
style: TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.bold),
),
subtitle: const Text(
'vn-industrial-zones',
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
),
activeTrackColor: const Color(0xFF38BDF8),
contentPadding: EdgeInsets.zero,
onChanged: (bool val) {
setState(() {
if (val) {
if (!_activeLayerCodes.contains('vn-industrial-zones')) {
_activeLayerCodes.add('vn-industrial-zones');
}
} else {
_activeLayerCodes.remove('vn-industrial-zones');
}
});
},
),
const SizedBox(height: 8),
// Custom text input for adding other codes
Row(
children: [
Expanded(
child: TextField(
style: const TextStyle(color: Colors.white, fontSize: 13),
decoration: InputDecoration(
hintText: 'Enter custom layer code...',
hintStyle: const TextStyle(color: Color(0xFF64748B), fontSize: 12),
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
filled: true,
fillColor: const Color(0xFF0F172A),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF334155)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF38BDF8)),
),
),
onSubmitted: (value) {
final code = value.trim();
if (code.isNotEmpty && !_activeLayerCodes.contains(code)) {
setState(() {
_activeLayerCodes.add(code);
});
}
},
),
),
],
),
if (_activeLayerCodes.isNotEmpty) ...[
const SizedBox(height: 12),
Wrap(
spacing: 6,
runSpacing: 6,
children: _activeLayerCodes.map((code) {
return InputChip(
label: Text(
code,
style: const TextStyle(fontSize: 11, color: Colors.white),
),
backgroundColor: const Color(0xFF0284C7).withValues(alpha: 0.3),
side: const BorderSide(color: Color(0xFF0284C7)),
onDeleted: () {
setState(() {
_activeLayerCodes.remove(code);
});
},
deleteIcon: const Icon(Icons.close_rounded, size: 14, color: Colors.white70),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}).toList(),
),
],
const SizedBox(height: 24),
// Quick Location bookmarks
const Text(
'QUICK DESTINATIONS',
style: TextStyle(
color: Color(0xFF64748B),
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const SizedBox(height: 8),
_buildBookmarkButton(
'Thang Long IZ (Hanoi)',
const LatLng(21.1350, 105.7890),
),
const SizedBox(height: 6),
_buildBookmarkButton(
'Hanoi Office',
const LatLng(21.0285, 105.8542),
),
const SizedBox(height: 6),
_buildBookmarkButton(
'Ho Chi Minh Hub',
const LatLng(10.7626, 106.6601),
),
const SizedBox(height: 6),
_buildBookmarkButton(
'Da Nang Branch',
const LatLng(16.0544, 108.2022),
),
if (_clickedLocation != null) ...[
const SizedBox(height: 24),
const Text(
'LAST CLOCKED COORDINATE',
style: TextStyle(
color: Color(0xFF64748B),
fontSize: 10,
fontWeight: FontWeight.bold,
letterSpacing: 0.5,
),
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF0F172A),
borderRadius: BorderRadius.circular(10),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'LAT: ${_clickedLocation!.latitude.toStringAsFixed(6)}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontFamily: 'Courier',
),
),
const SizedBox(height: 4),
Text(
'LNG: ${_clickedLocation!.longitude.toStringAsFixed(6)}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontFamily: 'Courier',
),
),
],
),
),
],
],
),
),
),
),
);
}
Widget _buildBookmarkButton(String label, LatLng position) {
return ElevatedButton.icon(
onPressed: () => _centerTo(position),
icon: const Icon(Icons.location_on_rounded, size: 16),
label: Text(label),
style: ElevatedButton.styleFrom(
alignment: Alignment.centerLeft,
backgroundColor: const Color(0xFF0F172A),
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
);
}
Widget _buildStatusToast() {
return Positioned(
top: 16,
right: 16,
child: SafeArea(
child: PointerInterceptor(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xFF1E293B).withValues(alpha: 0.9),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF334155)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFF34D399), // green status dot
shape: BoxShape.circle,
),
),
const SizedBox(width: 10),
Text(
_mapStatus,
style: const TextStyle(
color: Color(0xFFE2E8F0),
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
);
}
}