PowerMap Flutter SDK
Cross-platform map rendering for iOS and Android in Flutter.
Add the package to your pubspec.yaml:
dependencies:
powermap_sdk: ^0.2.0
Then, run:
flutter pub get
The PowerMap Flutter SDK is built with a Layered Facade Architecture, ensuring that high-performance native rendering is wrapped in an easy-to-use Flutter API.
graph TD
App[Flutter App] --> PowerMap[PowerMap Widget]
PowerMap --> Controller[PowerMapController]
subgraph "Facade Layer"
Controller
end
subgraph "Manager Layer"
Controller --> MarkerMgr[MarkerManager]
Controller --> SearchMgr[SearchManager]
Controller --> RouteMgr[RoutingManager]
Controller --> GeomMgr[GeometryManager]
Controller --> TrackingMgr[TrackingManager]
Controller --> LayerMgr[LayerManager]
end
subgraph "Service Layer"
MarkerMgr --> TileSvc[TileService]
SearchMgr --> GeoSvc[GeocodingProvider]
TileSvc --> Cache[3-Layer Cache]
end
subgraph "Native Engine"
PowerMap --> NativeEngine[High-Performance Native Rendering]
end
Cache -.-> Disk[(Local Storage)]
Cache -.-> CDN((PowerMap CDN))
-
Unified Facade:
PowerMapControllerprovides a single entry point for all features, matching the Web SDK's DX. - Native Rendering: Symbols, Lines, and Polygons are rendered in C++/OpenGL via our custom native engine for 60FPS performance.
- 3-Layer Caching: Automated Memory/Disk/Network caching ensures offline resilience and fast style loading.
- Hybrid Overlay: Arbitrary Flutter Widgets can be used as markers, perfectly synced with the map camera.
Get a map up and running in less than 2 minutes. The SDK provides a
powerful PowerMapController Facade.
Step A: Initialize Dual-Auth
Setup the SDK with your API keys globally in your
main.dart.
import 'package:flutter/material.dart';
import 'package:powermap_sdk/powermap_sdk.dart';
void main() {
PowerMapSDK.initialize(
mapApiKey: "YOUR_MAP_API_KEY", // Public key for tiles/base layers
clientId: "YOUR_CLIENT_ID", // Core Service Credentials
clientSecret: "YOUR_CLIENT_SECRET",
);
runApp(const MyApp());
}
Step B: The Map Widget
class MapPage extends StatefulWidget {
@override
_MapPageState createState() => _MapPageState();
}
class _MapPageState extends State<MapPage> {
PowerMapController? map;
@override
Widget build(BuildContext context) {
return Scaffold(
body: PowerMap(
initialCenter: const LatLng(13.7563, 100.5018), // Bangkok
initialZoom: 14.0,
mapStyle: 'dark', // 'th', 'en', 'dark', 'gray'
onMapCreated: (controller) => setState(() => map = controller),
onMapClick: (coords) => print("Tapped at: $coords"),
),
);
}
}
PowerMap (Widget Properties)
| Parameter | Type | Default | Description |
|---|---|---|---|
initialCenter |
LatLng |
LatLng(13.7563, 100.5018) |
The initial center point of the map. |
initialZoom |
double |
14.0 |
Initial viewport zoom level (0-22). |
mapStyle |
String |
'th' |
Base tile style. Available: 'th',
'en', 'dark',
'gray'.
|
onMapCreated |
Function(PowerMapController) |
null |
Triggered when the GL context is fully initialized. |
onMapClick |
Function(LatLng) |
null |
Coordinate where the user tapped. |
onMapLongClick |
Function(LatLng) |
null |
Coordinate where user long-pressed. |
onPoiTapped |
Function(Map<String, dynamic>)
|
null |
Fired when a base map POI or custom Geometry is tapped. Returns GeoJSON feature. |
onError |
Function(PowerMapException) |
null |
Global unified callback to catch map loading, network, or auth errors natively. |
The SDK renders native Symbol Layers for highest drawing performance. Markers support full customization matching the Web SDK's capabilities.
Core Marker Methods
| Method | Arguments | Description |
|---|---|---|
addMarker(position, ...) |
LatLng, {color?, iconImage?, size?, data?, opacity?}
|
Adds a GL-rendered map symbol marker. Best for performance. |
addWidgetMarker(position, widget, ...)
|
LatLng, widget, {id?, alignment?}
|
Overlays a Flutter Widget as a marker. Best for complex UI. |
setMarkersOpacity(opacity) |
double (0.0–1.0) |
Bulk-sets the opacity of all native markers.
Use with AnimationController for smooth fade
effects.
|
clearMarkers() |
- | Removes all GL symbols and circles. |
clearWidgetMarkers() |
- | Removes all Flutter widget markers. |
1. GL Symbol Markers (High Performance)
Best for large datasets (100+ points). Rendered directly by the GPU.
await map?.addMarker(
LatLng(13.76, 100.50),
color: '#f97316',
iconImage: 'packages/powermap_sdk/assets/markers/0101.png',
);
addMarker() Options
| Option | Type | Default | Description |
|---|---|---|---|
position |
LatLng |
Required | Coordinates for the marker. |
color |
String? |
null |
Hex color code. If set without iconImage, renders
a GL Circle pinpoint.
|
iconImage |
String? |
marker-o.png |
Asset path for the icon. Uses bundled default if omitted. |
size |
double |
1.0 |
Scale factor for the icon or circle radius. |
data |
Map<String, dynamic>? |
null |
Metadata attached to the marker (accessible on tap). |
opacity |
double |
1.0 |
Initial visibility of the marker (0.0 = invisible, 1.0 = fully
visible). Use with setMarkersOpacity() to animate
a fade-in.
|
2. Flutter Widget Markers (High Interactivity)
Best for complex UIs, buttons, or animations inside a marker. These
markers are overlaid in a Stack and synchronized with
the map's camera using pixel projection.
map?.addWidgetMarker(
LatLng(13.75, 100.48),
Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(blurRadius: 4, color: Colors.black26)],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.restaurant, color: Colors.orange, size: 16),
SizedBox(width: 4),
Text("Delicious Cafe", style: TextStyle(fontSize: 12)),
],
),
),
alignment: Alignment.bottomCenter,
);
addWidgetMarker() Options
| Option | Type | Default | Description |
|---|---|---|---|
position |
LatLng |
Required | Geographic location to pin the widget. |
widget |
Widget |
Required | The Flutter widget to display. |
id |
String? |
null |
Optional unique identifier. |
alignment |
Alignment |
Alignment.center |
Anchor point of the widget relative to the
position.
|
Bundled Icon Assets
The SDK ships with 290+ marker icons copied
directly from the Web SDK's asset library
(assets/markers/). These include category icons, route
waypoint markers, and the default pinpoint.
// 1. Default marker (uses bundled marker-o.png automatically)
await map?.addMarker(LatLng(13.7563, 100.5018));
// 2. Custom colored GL pinpoint (no image needed, like Web SDK)
await map?.addMarker(
LatLng(13.76, 100.50),
color: '#f97316', // Vibrant Orange
size: 1.5,
data: {'name': 'My Store'},
);
// 3. Custom icon from bundled assets
await map?.addMarker(
LatLng(13.77, 100.51),
iconImage: 'packages/powermap_sdk/assets/markers/0101.png',
);
// 4. High-Performance Clustering
final List<LatLng> stores = [ ... ];
await map?.setClusters(stores);
3. Marker Opacity & Fade Animation (v0.2.0+)
Eliminate the visual "flash" that occurs when clearing and
re-adding markers by using the opacity parameter
together with setMarkersOpacity() and a Flutter
AnimationController.
How it works:
-
clearMarkers()— removes all old markers instantly -
addMarker(..., opacity: 0.0)— adds new markers invisibly -
setMarkersOpacity(value)driven byAnimationController— fades all markers in smoothly
// In your StatefulWidget, declare:
late final AnimationController _fadeCtrl;
@override
void initState() {
super.initState();
_fadeCtrl = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
);
}
// Render markers with smooth fade-in:
Future<void> renderMarkersWithFade(List<LatLng> positions) async {
// Step 1: Clear old markers
await map?.clearMarkers();
// Step 2: Add all new markers invisibly
for (final pos in positions) {
await map?.addMarker(
pos,
color: '#E42320',
opacity: 0.0, // Start invisible
);
}
// Step 3: Fade in all markers together
_fadeCtrl.addListener(() {
map?.setMarkersOpacity(_fadeCtrl.value);
});
_fadeCtrl.forward(from: 0); // Animates 0.0 → 1.0 over 350ms
}
@override
void dispose() {
_fadeCtrl.dispose();
super.dispose();
}
setMarkersOpacity() updates all GL Circles and Symbols
in a single batch — there is no per-marker loop required from the
application side.
Directly manipulate the map viewport matching the fluidity of the Web SDK.
Movement Methods
| Method | Arguments | Description |
|---|---|---|
moveCamera(target, ...) |
LatLng, zoom?, tilt?, bearing?
|
Smooth cinematic fly-to animation with optional rotations. |
zoomIn() |
- | Increments zoom level by 1.0 with smooth animation. |
zoomOut() |
- | Decrements zoom level by 1.0 with smooth animation. |
setTilt(deg) |
double |
Set tilt angle (0 = 2D, 60+ = 3D). |
setBearing(deg) |
double |
Set direction (0 = North). |
tiltUp() |
- | Shortcut to animate the camera to a 3D perspective view. |
tiltDown() |
- | Shortcut to animate back to a top-down view. |
State Functions
| Method | Returns | Description |
|---|---|---|
getCenter() |
LatLng? |
Returns current view center. |
getZoom() |
double? |
Returns current map zoom level. |
getBearing() |
double? |
Returns current map rotation. |
getTilt() |
double? |
Returns current camera pitch. |
await map?.moveCamera(
LatLng(13.7649, 100.5383),
zoom: 17.0,
tilt: 60.0,
bearing: 90.0,
);
The Facade Pattern
For maximum DX, you do not need to dive into individual managers.
All primary actions (search, addMarker,
addRoute, moveCamera,
setLayers, getGeoJSON) are available
directly on the PowerMapController instance:
map?.search("Mall");
map?.getGeoJSON("poi-123");
map?.addRoute(...);
Calculate logistics-grade paths containing step-by-step metadata.
addRoute() Options
| Option | Type | Default | Description |
|---|---|---|---|
start |
LatLng |
Required | Origin coordinate. |
destination |
LatLng |
Required | Final destination coordinate. |
waypoints |
List<LatLng> |
[] |
Intended stops between origin and destination. |
optimize |
bool |
false |
If true, applies TSP logic to reorder waypoints for efficiency. Minimum 1 waypoint required. |
profile |
String |
'driving' |
Transportation mode: 'driving',
'walking',
'cycling'.
|
language |
String |
'th' |
Return instructions in 'th' or
'en'.
|
PowerMapRouteResult Object
| Property | Type | Description |
|---|---|---|
distance |
double |
Route length in meters. |
duration |
double |
Estimated seconds en route. |
distanceStr |
String |
Human-readable (e.g. "5.2 km"). |
durationStr |
String |
Human-readable (e.g. "12 mins"). |
steps |
List<PowerRouteStep> |
Step-by-step turn objects (contains text,
distanceStr, location).
|
geometry |
List<LatLng> |
Raw polyline coordinates array. |
final route = await map?.routing.addRoute(
start: LatLng(13.7468, 100.5346),
destination: LatLng(13.7649, 100.5383),
optimize: true
);
print("Route calculated: ${route?.distanceStr}");
The Mobile SDK incorporates a fully custom
GeocodingProvider interface. It mirrors the exact
capabilities of the Web SDK while adding safety boundaries like
Debouncing and LRU Caching.
Search Methods
| Method | Arguments | Description |
|---|---|---|
search() |
query, location?, lang? |
Standard one-off search. Checks local LRU Cache first. |
searchByCategory() |
categoryId, location, {dist?, lang?}
|
Searches for places nearby by category ID (e.g.
'2100' = Restaurant).
|
autocompleteDebounced() |
query, delay, onResult, location?
|
Safe to bind to TextField.onChanged. Custom delay
prevents quota burn.
|
reverseGeocode() |
location, lang? |
Converts coordinates into a structured address
(PowerMapSearchResult).
|
clearSearchCache() |
- | Clears the internal LRU cache manually. |
Language Detection (lang Parameter)
The lang parameter controls which search endpoint is
used. The demo app automatically detects the language from the query
text and passes the appropriate value:
lang value |
Endpoint | When to use |
|---|---|---|
'th' (default)
|
/api/v2/map/address_th |
Query contains Thai characters only |
'en' |
/api/v2/map/address_en |
Query contains Latin/English characters only |
'mixed' |
/api/v2/map/address |
Query contains both Thai and English |
U+0E00–U+0E7F) and
Latin (U+0041–U+007A). If a query contains both,
'mixed' is passed to use the unified endpoint.
PowerMapSearchResult Model
| Property | Type | Description |
|---|---|---|
name |
String |
Primary title of the location. |
address |
String |
Full formatted address string. |
position |
LatLng |
Exact geographic coordinates. |
raw |
Map<String, dynamic> |
Raw API response if backend schemas change. |
// Language is detected automatically in the demo:
String detectLang(String query) {
final hasLatin = query.runes.any((c) => (c >= 0x41 && c <= 0x5A) || (c >= 0x61 && c <= 0x7A));
final hasThai = query.runes.any((c) => c >= 0x0E00 && c <= 0x0E7F);
if (hasLatin && !hasThai) return 'en';
if (hasLatin && hasThai) return 'mixed';
return 'th';
}
// 1. Forward search with auto-detected lang
final results = await map?.search(query, lang: detectLang(query));
// 2. Debounced Autocomplete (safe for TextField)
map?.autocompleteDebounced(
query,
Duration(milliseconds: 500),
(results) {
if (results.isNotEmpty) {
print("Found: ${results.first.name} at ${results.first.position}");
}
}
);
// 3. Reverse Geocode (Lat/Lng to Address)
final address = await map?.reverseGeocode(LatLng(14.2558, 100.9810));
if (address != null) {
print("Address: ${address.name}, ${address.province}");
}
// 4. Category Search (Nearby POIs by type)
final myLocation = await map?.getCurrentLocation();
if (myLocation != null) {
final restaurants = await map?.searchByCategory('2100', myLocation, dist: 5);
print("Found ${restaurants?.length} restaurants nearby!");
}
The Mobile SDK provides a robust API for handling GeoJSON data. You can import, render, and retrieve your map features with ease.
Geometry API Reference
| Method | Arguments | Description |
|---|---|---|
getGeoJSON()
|
String? id, Map? opts |
The primary way to retrieve your data. Returns feature(s) as a Map object. |
exportGeometry() |
String? id, Map? opts |
Alias for getGeoJSON() for parity with the Web
SDK.
|
addGeoJson() |
sourceId, geoJsonMap |
Injects raw GeoJSON into the map source for later styling. |
importGeoJson() |
sourceId, geoJsonMap, ... |
Injects raw GeoJSON into the map and optionally renders it. |
addIndoorExtrusionLayer() |
sourceId, {layerId} |
Automatically renders 3D walls using the
height property.
|
addPolygonLayer() |
sourceId, {layerId} |
Draws standard 2D shapes (Fills and Outlines). Perfect for Geofences. |
addPolylineLayer() |
sourceId, {layerId} |
Renders paths and lines with scalable widths
(LineString).
|
Code Example: Data Retrieval
// 1. Get ALL geometry current in the session
final allFeatures = await map?.getGeoJSON();
// 2. Get a SPECIFIC feature by ID with format options
final specificFeature = await map?.getGeoJSON(
'poi-123',
options: { 'format': 'standard' }
);
Code Example: Data Ingestion & Layer Creation
// 1. Prepare your Geometry Data (GeoJSON format: Lng, Lat)
final geofenceData = {
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[100.534, 13.746], [100.538, 13.746],
[100.538, 13.749], [100.534, 13.749],
[100.534, 13.746] // Must close the loop
]]
}
};
// 2. Inject Data into the Map Source
await map?.geometry.addGeoJson("my-geofence", geofenceData);
// 3. Apply Visual Styles to that Source ID
await map?.geometry.addPolygonLayer(
"my-geofence",
fillColor: "#4264fb",
fillOpacity: 0.5,
outlineColor: "#000000"
);
Powered by Geolocator for battery-efficient, high-accuracy GPS positioning. Designed for delivery apps and navigation use cases without burning battery on heavy Dart timers.
Tracking Modes
| Mode | Description |
|---|---|
TrackingMode.none |
Camera is unlocked — user can pan freely. |
TrackingMode.follow |
Camera follows device location smoothly. |
TrackingMode.followWithCompass
|
Camera follows + rotates to match device heading (3D navigation view). |
Core Methods
| Method | Returns | Description |
|---|---|---|
enableFollowMode() |
Future<void> |
Locks camera smoothly on device GPS location. Auto-requests permission. |
enableFollowModeWithCompass() |
Future<void> |
Locks camera + rotates map to match device heading with 3D tilt. |
disableFollowMode() |
Future<void> |
Unlocks camera, stops GPS stream to conserve battery. |
getCurrentLocation() |
Future<LatLng?> |
One-shot GPS request. Useful for "My Location" button. |
requestPermission() |
Future<bool> |
Checks & requests location permission. Returns
true if granted.
|
Usage Examples
// 1. Follow device location smoothly
await map?.enableFollowMode();
// 2. Navigation mode with compass heading (3D view)
await map?.enableFollowModeWithCompass();
// 3. Stop tracking to save battery
await map?.disableFollowMode();
// 4. One-shot: Get current coordinates
final myLocation = await map?.getCurrentLocation();
if (myLocation != null) {
print("I'm at: $myLocation");
}
// 5. Listen to position updates reactively
map?.tracking.positionNotifier.addListener(() {
final pos = map?.tracking.positionNotifier.value;
if (pos != null) {
print("Lat: ${pos.latitude}, Lng: ${pos.longitude}");
}
});
[!IMPORTANT] Battery Efficiency: The GPS stream uses
distanceFilter: 5meters — updates only fire when the device actually moves ≥5m. This prevents unnecessary CPU/battery drain compared to timer-based polling.
[!NOTE] Permissions:
enableFollowMode()andenableFollowModeWithCompass()automatically request location permissions. You can also callrequestPermission()manually during onboarding to pre-ask the user.
To compile the SDK:
- Clone Repository.
flutter pub get.- Check
lib/managers/for logic separation. -
Run
dart analyzeto ensure code meets standards.
The SDK provides a unified PowerMapException class for
handling all types of failures. Instead of generic errors, you get
specific codes and descriptive messages.
PowerMapException Properties
| Property | Type | Description |
|---|---|---|
code |
String |
Programmatic error code (e.g., NETWORK_ERROR).
|
message |
String |
Human-readable explanation in English. |
details |
dynamic |
The underlying raw error (Stacktrace or Response body). |
Common Error Codes
| Code | Triggered By | Reason |
|---|---|---|
AUTH_FAILED |
Any API |
Missing/Incorrect mapApiKey or
clientSecret.
|
NETWORK_ERROR |
Any API | No internet connection or server unreachable. |
STYLE_LOAD_FAILED |
Map View | Failed to load the optimized style JSON. |
ROUTE_NOT_FOUND |
Routing | No path exists between the selected points. |
SERVER_ERROR |
Any API | Backend returned a 500 error. |
Handling Errors in Code
try {
final results = await map.search("Siam Paragon");
} on PowerMapException catch (e) {
if (e.code == PowerMapException.network) {
print("Please check your connection!");
} else {
print("PowerMap Error: ${e.message}");
}
}
© 2026 PowerMap Development Team. All rights reserved.