React Native bridge for AMap (高德地图) iOS/Android SDK with full New Architecture (Fabric) support and built-in Expo config plugin.
- Features
- Installation
- Getting Your API Keys
- Quick Start
- Configuration
- API Reference
- Examples
- Troubleshooting
- Requirements
- Contributing
- License
- ✅ New Architecture Ready - Full support for React Native 0.81+ with Fabric and TurboModules
- 🎯 Expo Config Plugin - Zero-config setup for Expo managed workflow
- 🗺️ Map Display - Standard and satellite map types with full gesture controls
- 📍 Markers - Add, remove, and customize markers with callouts
- 🎨 Custom Icons - Support for custom marker icons from URLs or local assets
- 🔢 Marker Clustering - Automatic marker clustering with customizable appearance
- 📐 Overlays - Polylines, polygons, and circles with customizable styles
- 📱 User Location - Show user's current location on the map
- 🎯 Camera Control - Programmatic camera positioning with smooth animations
- 📡 Events - Rich event system for map interactions
The easiest way to use this library in Expo projects - just 2 steps!
npx expo install @jindun619/react-native-amapThe config plugin will be automatically detected by Expo CLI!
Option A: Using environment variables (Recommended)
Create a .env file in your project root:
# .env (add this file to .gitignore!)
EXPO_PUBLIC_AMAP_IOS_API_KEY=your_ios_api_key_here
EXPO_PUBLIC_AMAP_ANDROID_API_KEY=your_android_api_key_hereOption B: Using app.config.js
// app.config.js
export default {
expo: {
// ... other config
plugins: [
[
'@jindun619/react-native-amap',
{
iosApiKey: 'your_ios_api_key',
androidApiKey: 'your_android_api_key',
// Optional: custom permission descriptions
iosLocationWhenInUseDescription: 'We need your location to show your position on the map',
iosLocationAlwaysDescription: 'We need your location to show your position on the map'
}
]
]
}
};# Prebuild to apply native changes
npx expo prebuild
# Run on your device/simulator
npx expo run:ios
# or
npx expo run:androidThat's it! 🎉 The config plugin automatically handles:
- ✅ iOS Info.plist configuration (API key, location permissions, App Transport Security)
- ✅ iOS AppDelegate initialization (AMap SDK privacy compliance)
- ✅ Android AndroidManifest.xml setup (API key, permissions)
- ✅ Android build.gradle Maven repository configuration
npm install @jindun619/react-native-amap
# or
yarn add @jindun619/react-native-amap- Install pods:
cd ios && pod install- Add AMap API key to
Info.plist:
<key>AMapApiKey</key>
<string>YOUR_AMAP_IOS_API_KEY</string>- Add location permissions to
Info.plist:
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show your position on the map</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need your location to show your position on the map</string>- Add App Transport Security exception to
Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>amap.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>⚠️ CRITICAL: Initialize AMap SDK in AppDelegate
Without this step, the map will not display!
For Objective-C (AppDelegate.mm):
#import <AMapFoundationKit/AMapFoundationKit.h>
#import <MAMapKit/MAMapKit.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Configure AMap SDK privacy compliance (MUST be called before MAMapView instantiation)
[MAMapView updatePrivacyShow:AMapPrivacyShowStatusDidShow privacyInfo:AMapPrivacyInfoStatusDidContain];
[MAMapView updatePrivacyAgree:AMapPrivacyAgreeStatusDidAgree];
[[AMapServices sharedServices] setEnableHTTPS:YES];
// ... rest of your AppDelegate code
return YES;
}For Swift (AppDelegate.swift):
import AMapFoundationKit
import MAMapKit
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Configure AMap SDK privacy compliance
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
MAMapView.updatePrivacyAgree(.didAgree)
AMapServices.shared().enableHTTPS = true
// ... rest of your AppDelegate code
return true
}- Add AMap Maven repository to
android/build.gradle:
allprojects {
repositories {
// ... other repositories
maven { url "https://maven.aliyun.com/repository/public" }
}
}- Add permissions and API key to
AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Required permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application>
<!-- AMap API Key -->
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="YOUR_AMAP_ANDROID_API_KEY" />
<!-- ... rest of your application config -->
</application>
</manifest>You need separate API keys for iOS and Android from AMap:
- Go to AMap Developer Console
- Create an account or sign in
- Create a new application
- Get your iOS and Android API keys (they are different!)
Important:
- iOS and Android require separate API keys
- Keep your API keys secure (use
.envfile and add it to.gitignore)
import React, { useRef } from 'react';
import { StyleSheet, View } from 'react-native';
import { AmapView, type AmapViewHandle } from '@jindun619/react-native-amap';
export default function App() {
const mapRef = useRef<AmapViewHandle>(null);
return (
<View style={styles.container}>
<AmapView
ref={mapRef}
style={styles.map}
mapType="standard"
showsUserLocation={true}
onMapReady={() => console.log('Map is ready!')}
onMapPress={(event) => console.log('Map pressed:', event.coordinate)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
});import { useEffect, useRef } from 'react';
import { AmapView, type AmapViewHandle } from '@jindun619/react-native-amap';
export default function MapWithMarkers() {
const mapRef = useRef<AmapViewHandle>(null);
useEffect(() => {
// Add marker when map is ready
const addMarker = async () => {
await mapRef.current?.addMarker({
id: 'beijing',
coordinate: { latitude: 39.9042, longitude: 116.4074 },
title: 'Beijing',
subtitle: 'Capital of China',
showsCallout: true,
});
// Animate camera to marker
await mapRef.current?.animateToRegion(39.9042, 116.4074, 15, 1000);
};
addMarker();
}, []);
return (
<AmapView
ref={mapRef}
style={{ flex: 1 }}
showsUserLocation={true}
onMarkerPress={(event) => console.log('Marker pressed:', event.id)}
/>
);
}When using Expo, you can configure the plugin in app.config.js:
export default {
expo: {
plugins: [
[
'@jindun619/react-native-amap',
{
// Required: API keys
iosApiKey: 'your_ios_key', // or use EXPO_PUBLIC_AMAP_IOS_API_KEY env var
androidApiKey: 'your_android_key', // or use EXPO_PUBLIC_AMAP_ANDROID_API_KEY env var
// Optional: Custom permission descriptions
iosLocationWhenInUseDescription: 'Custom description for location permission',
iosLocationAlwaysDescription: 'Custom description for always location permission',
}
]
]
}
};For better security, use environment variables:
# .env
EXPO_PUBLIC_AMAP_IOS_API_KEY=your_ios_key
EXPO_PUBLIC_AMAP_ANDROID_API_KEY=your_android_keyThe plugin will automatically read these variables if API keys are not provided in the config.
| Prop | Type | Default | Description |
|---|---|---|---|
mapType |
'standard' | 'satellite' |
'standard' |
Map display type |
showsBuildings |
boolean |
true |
Show 3D buildings |
showsTraffic |
boolean |
false |
Show traffic layer |
showsLabels |
boolean |
true |
Show text labels |
showsUserLocation |
boolean |
false |
Show user's location |
| Prop | Type | Default | Description |
|---|---|---|---|
zoomEnabled |
boolean |
true |
Enable pinch to zoom |
scrollEnabled |
boolean |
true |
Enable pan gestures |
rotateEnabled |
boolean |
true |
Enable rotation gestures |
tiltEnabled |
boolean |
true |
Enable 3D tilt gestures |
| Prop | Type | Default | Description |
|---|---|---|---|
clusteringEnabled |
boolean |
false |
Enable automatic marker clustering |
Access these methods via ref:
const mapRef = useRef<AmapViewHandle>(null);// Animate to a specific location
await mapRef.current?.animateToRegion(
latitude: number,
longitude: number,
zoom: number,
duration?: number // milliseconds (optional)
);
// Set camera position (no animation)
await mapRef.current?.setCamera({
latitude: number,
longitude: number,
zoom: number,
tilt?: number, // 0-60 degrees (optional)
rotation?: number // 0-360 degrees (optional)
});// Add a marker
await mapRef.current?.addMarker({
id: string,
coordinate: { latitude: number, longitude: number },
title?: string,
subtitle?: string,
showsCallout?: boolean,
icon?: string // URL, asset name, or require()
});
// Remove a marker
await mapRef.current?.removeMarker(id: string);
// Clear all markers
await mapRef.current?.clearMarkers();
// Show/hide marker callout
await mapRef.current?.showCallout(id: string);
await mapRef.current?.hideCallout(id: string);Polyline:
await mapRef.current?.addPolyline({
id: string,
coordinates: Array<{ latitude: number, longitude: number }>,
strokeColor?: string, // hex color (e.g., '#FF0000')
strokeWidth?: number // in pixels
});
await mapRef.current?.removePolyline(id: string);
await mapRef.current?.clearPolylines();Polygon:
await mapRef.current?.addPolygon({
id: string,
coordinates: Array<{ latitude: number, longitude: number }>,
strokeColor?: string,
strokeWidth?: number,
fillColor?: string // hex color with alpha (e.g., '#FF000080')
});
await mapRef.current?.removePolygon(id: string);
await mapRef.current?.clearPolygons();Circle:
await mapRef.current?.addCircle({
id: string,
center: { latitude: number, longitude: number },
radius: number, // in meters
strokeColor?: string,
strokeWidth?: number,
fillColor?: string
});
await mapRef.current?.removeCircle(id: string);
await mapRef.current?.clearCircles();| Event | Payload | Description |
|---|---|---|
onMapReady |
void |
Called when map is loaded and ready |
onRegionChange |
RegionChangeEvent |
Called when visible region changes |
onMapPress |
MapPressEvent |
Called when map is tapped |
onMapLongPress |
MapPressEvent |
Called when map is long pressed |
onMarkerPress |
MarkerPressEvent |
Called when marker is tapped |
onClusterPress |
ClusterPressEvent |
Called when marker cluster is tapped |
interface LatLng {
latitude: number;
longitude: number;
}
interface Marker {
id: string;
coordinate: LatLng;
title?: string;
subtitle?: string;
showsCallout?: boolean;
icon?: string | number;
}
interface CameraPosition {
latitude: number;
longitude: number;
zoom: number;
tilt?: number; // 0-60 degrees
rotation?: number; // 0-360 degrees
}
interface MapPressEvent {
coordinate: LatLng;
}
interface MarkerPressEvent {
id: string;
coordinate: LatLng;
}
interface RegionChangeEvent {
latitude: number;
longitude: number;
latitudeDelta: number;
longitudeDelta: number;
zoom: number;
}
interface ClusterPressEvent {
coordinate: LatLng;
markerCount: number;
}
interface Polyline {
id: string;
coordinates: LatLng[];
strokeColor?: string;
strokeWidth?: number;
}
interface Polygon {
id: string;
coordinates: LatLng[];
strokeColor?: string;
strokeWidth?: number;
fillColor?: string;
}
interface Circle {
id: string;
center: LatLng;
radius: number; // in meters
strokeColor?: string;
strokeWidth?: number;
fillColor?: string;
}import React, { useRef, useState } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import { AmapView, type AmapViewHandle } from '@jindun619/react-native-amap';
export default function App() {
const mapRef = useRef<AmapViewHandle>(null);
const [clusteringEnabled, setClusteringEnabled] = useState(false);
const [markerCount, setMarkerCount] = useState(0);
const handleAddMarker = async () => {
const id = `marker-${markerCount}`;
await mapRef.current?.addMarker({
id,
coordinate: {
latitude: 39.9042 + (Math.random() - 0.5) * 0.1,
longitude: 116.4074 + (Math.random() - 0.5) * 0.1
},
title: `Marker ${markerCount + 1}`,
subtitle: `ID: ${id}`,
showsCallout: true,
});
setMarkerCount(markerCount + 1);
};
const handleAddPolyline = async () => {
await mapRef.current?.addPolyline({
id: 'route-1',
coordinates: [
{ latitude: 39.9042, longitude: 116.4074 },
{ latitude: 39.9142, longitude: 116.4174 },
{ latitude: 39.9242, longitude: 116.4274 }
],
strokeColor: '#FF0000',
strokeWidth: 5
});
};
const handleAddCircle = async () => {
await mapRef.current?.addCircle({
id: 'area-1',
center: { latitude: 39.9042, longitude: 116.4074 },
radius: 1000,
strokeColor: '#0000FF',
strokeWidth: 2,
fillColor: '#0000FF40'
});
};
const handleGoToBeijing = async () => {
await mapRef.current?.animateToRegion(39.9042, 116.4074, 12, 1000);
};
return (
<View style={styles.container}>
<AmapView
ref={mapRef}
style={styles.map}
mapType="standard"
showsUserLocation={true}
showsTraffic={false}
clusteringEnabled={clusteringEnabled}
onMapReady={() => console.log('Map ready!')}
onMapPress={(event) => console.log('Map pressed:', event.coordinate)}
onMarkerPress={(event) => console.log('Marker pressed:', event.id)}
onClusterPress={(event) =>
console.log(`Cluster: ${event.markerCount} markers`)
}
onRegionChange={(event) => console.log('Region changed:', event)}
/>
<View style={styles.controls}>
<Button title="Add Marker" onPress={handleAddMarker} />
<Button title="Add Polyline" onPress={handleAddPolyline} />
<Button title="Add Circle" onPress={handleAddCircle} />
<Button title="Go to Beijing" onPress={handleGoToBeijing} />
<Button
title={clusteringEnabled ? 'Disable Clustering' : 'Enable Clustering'}
onPress={() => setClusteringEnabled(!clusteringEnabled)}
/>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { flex: 1 },
controls: {
position: 'absolute',
bottom: 20,
left: 20,
right: 20,
gap: 10,
},
});// From URL
await mapRef.current?.addMarker({
id: 'marker-url',
coordinate: { latitude: 39.9042, longitude: 116.4074 },
icon: 'https://example.com/marker-icon.png'
});
// From local asset (iOS: Assets.xcassets, Android: drawable)
await mapRef.current?.addMarker({
id: 'marker-asset',
coordinate: { latitude: 39.9042, longitude: 116.4074 },
icon: 'marker_icon' // asset name without extension
});<AmapView
ref={mapRef}
clusteringEnabled={true}
onClusterPress={(event) => {
console.log(`Cluster with ${event.markerCount} markers`);
console.log(`Location: ${event.coordinate.latitude}, ${event.coordinate.longitude}`);
}}
/>Clustering features:
- Automatic grouping of markers within 60 pixels
- Purple cluster markers with marker count
- Custom
onClusterPressevent - Re-clusters automatically when map moves
Most common issue: Missing AMap SDK initialization in AppDelegate (iOS)
Make sure you added the privacy compliance and HTTPS configuration in AppDelegate.mm or AppDelegate.swift:
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
MAMapView.updatePrivacyAgree(.didAgree)
AMapServices.shared().enableHTTPS = trueFor Expo users: The config plugin should handle this automatically. If the map still doesn't display:
- Run
npx expo prebuild --cleanto regenerate native code - Rebuild your development build
- Check that your API keys are correctly set in
.envorapp.config.js
1. Invalid API Key
- Make sure you're using the correct API key for iOS/Android
- iOS and Android keys are different - don't mix them up!
- Verify keys at AMap Developer Console
2. Missing permissions (Android)
- Ensure all required permissions are added to
AndroidManifest.xml - For Expo: plugin adds permissions automatically
3. Missing Maven repository (Android)
- Android needs the AMap Maven repository in
build.gradle - For Expo: plugin adds repository automatically
4. Pod install fails (iOS)
cd ios
rm -rf Pods Podfile.lock
pod install --repo-update5. Clean rebuild
# iOS
cd ios && rm -rf Pods Podfile.lock && pod install && cd ..
# Android
cd android && ./gradlew clean && cd ..
# Expo
npx expo prebuild --cleanThis is usually due to missing privacy compliance setup on iOS. Make sure the privacy methods are called before any map view is created.
- Check that location permissions are granted
- Verify
showsUserLocation={true}prop is set - On iOS, ensure
Info.plisthas location usage descriptions - Test on a real device (simulators may not have location)
# Clear cache and restart
npx expo start --clear
# Or reinstall
rm -rf node_modules
npm install
npx expo prebuild --clean# Clean everything and rebuild
yarn clean
rm -rf node_modules yarn.lock
yarn install
npx expo prebuild --clean- React Native >= 0.81.0 (New Architecture)
- iOS >= 12.0
- Android >= API 21
- AMap API Keys (separate for iOS and Android)
For Expo:
- Expo >= 54.0.0
- Development Build or Standalone Build (not Expo Go)
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Please follow the Conventional Commits specification for commit messages.
MIT
- Built with create-react-native-library
- Uses AMap iOS SDK
- Uses AMap Android SDK
Made with ❤️ by jindun619
