Getting Started
Complete guide for integrating Barikoi Maps into an existing React Native (Expo) application.
Prerequisites
| Requirement | Version tested |
|---|---|
| Node.js | ≥ 18 LTS |
| Expo CLI | npx expo (bundled with expo package) |
| EAS CLI | eas-cli ≥ 20.4.0 (for device / cloud builds) |
| Expo SDK | 54 (expo ~54.0.34) |
| React Native | 0.81.x (ships with Expo SDK 54) |
| Xcode | 16+ (iOS builds) |
| Android Studio | Hedgehog+ with NDK (Android builds) |
| Barikoi API key | Free tier available — register here |
Expo Go will not work. The MapLibre native module requires a custom dev client. You must use
expo prebuild+expo run:android/iosor EAS Build.
Installation
1. Get your API key
- Register at the Barikoi Developer Dashboard.
- Verify with your phone number.
- Claim your API key (starts with
bkoi_…).
Keep the key handy — you will reference it in your map style URL and API client.
2. Install dependencies
npx expo install \
@maplibre/maplibre-react-native@^11.3.6 \
expo-location@~19.0.8 \
expo-dev-client@~6.0.21
npm install barikoiapis@^2.0.2
If you are using TypeScript, also install the GeoJSON type definitions:
npm install -D @types/geojson
| Package | Purpose |
|---|---|
@maplibre/maplibre-react-native | Native MapLibre GL map view, camera, markers |
expo-location | Foreground/background location permissions & GPS |
expo-dev-client | Custom development client (required for native modules) |
barikoiapis | Official Barikoi TypeScript SDK — autocomplete, reverse geocode, routing, etc. |
Why
npx expo install? It pins versions compatible with your current Expo SDK. Use plainnpm installfor packages that aren't in the Expo version policy (e.g.barikoiapis).
expo-dev-client?Barikoi Maps uses native modules that cannot run in Expo Go. expo-dev-client lets you create a development build that supports native code.
3. Update app.json
Add the MapLibre Expo config plugin to the plugins array. This replaces all manual build.gradle / Podfile edits that are necessary in bare React Native. Add these entries inside your existing expo object — do not replace your entire app.json:
{
"expo": {
"name": "mapapp",
"slug": "mapapp",
"version": "1.0.0",
"newArchEnabled": true,
"plugins": [
"expo-router",
"@maplibre/maplibre-react-native", // ← required for native map module
[
"expo-splash-screen",
{ /* …your splash config… */ }
]
],
"extra": {
"router": {},
"eas": {
"projectId": "<YOUR_EAS_PROJECT_ID>"
}
}
}
}
Key differences from bare React Native:
| Bare RN | Expo |
|---|---|
Manually edit android/build.gradle, settings.gradle, MainApplication | Just add "@maplibre/maplibre-react-native" to plugins |
Manually edit ios/Podfile and run pod install | Plugin handles CocoaPods config during expo prebuild |
| Manually link native modules | Config plugins auto-link during prebuild |
The @maplibre/maplibre-react-native plugin entry is required — without it the native map module will not register correctly.
If you want to enable the New Architecture, add "newArchEnabled": true to your expo object. Be aware this may affect other native modules in your project. Test thoroughly before enabling it in an existing project.
4. Create the map utilities file
Create utils/mapUtils.ts in your project. This file centralises Barikoi-specific defaults so every screen stays consistent. Adjust the import path in your screen files based on where you place this file.
// utils/mapUtils.ts
/**
* Map style URLs — append your API key as a query parameter.
* Barikoi offers several vector-tile styles; osm-liberty is the default.
*/
export const MAP_STYLES = {
osmLiberty: (apiKey: string) =>
`https://map.barikoi.com/styles/osm-liberty/style.json?key=${apiKey}`,
// Add other Barikoi styles here as they become available
} as const;
/**
* Default centre point: Dhaka, Bangladesh.
* Coordinates are in GeoJSON order: [longitude, latitude].
*/
export const DEFAULT_CENTER: [number, number] = [90.4125, 23.8103];
/**
* Sensible zoom defaults.
*/
export const DEFAULT_ZOOM = 12;
export const SELECTED_ZOOM = 16;
export const USER_LOCATION_ZOOM = 14;
export const CLOSE_ZOOM = 15;
/**
* Camera animation defaults (milliseconds).
*/
export const FLY_DURATION_MS = 1000;
/**
* Brand / accent colour used throughout the UI.
*/
export const BRAND_BLUE = '#007AFF';
// ── Helper functions ────────────────────────────────────────────────
/**
* Parse a string-or-number value into a finite float.
* Returns `NaN` for null / undefined / non-numeric strings.
*/
export function safeParseFloat(value: string | number | undefined): number {
return parseFloat(String(value));
}
/**
* Return true when both lon and lat are finite numbers inside valid ranges.
*/
export function isValidCoordinate(lon: number, lat: number): boolean {
return (
Number.isFinite(lon) &&
Number.isFinite(lat) &&
lon >= -180 &&
lon <= 180 &&
lat >= -90 &&
lat <= 90
);
}
/**
* Haversine distance between two [lon, lat] points in metres.
*/
export function haversineDistance(
a: [number, number],
b: [number, number],
): number {
const toRad = (deg: number) => (deg * Math.PI) / 180;
const R = 6_371_000; // Earth radius in metres
const dLat = toRad(b[1] - a[1]);
const dLon = toRad(b[0] - a[0]);
const sinLat = Math.sin(dLat / 2);
const sinLon = Math.sin(dLon / 2);
const h =
sinLat * sinLat +
Math.cos(toRad(a[1])) * Math.cos(toRad(b[1])) * sinLon * sinLon;
return R * 2 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
}
/**
* Basic point-in-polygon (ray-casting) for [lon, lat] arrays.
*/
export function pointInPolygon(
point: [number, number],
polygon: [number, number][],
): boolean {
let inside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
const xi = polygon[i][0], yi = polygon[i][1];
const xj = polygon[j][0], yj = polygon[j][1];
const intersect =
yi > point[1] !== yj > point[1] &&
point[0] < ((xj - xi) * (point[1] - yi)) / (yj - yi) + xi;
if (intersect) inside = !inside;
}
return inside;
}
5. Build and run
Because @maplibre/maplibre-react-native includes native code, you must rebuild the native projects whenever you add or change config plugins.
Any time you change the plugins array in app.json, or change newArchEnabled, you must create a new native build. A Metro reload or hot reload is not enough.
Local development (physical device or emulator)
# Generate native projects (runs the config plugins)
npx expo prebuild
# Run on Android
npx expo run:android
# Run on iOS
npx expo run:ios
After the first build, start the JS bundler with:
npx expo start
Cloud builds with EAS
# Install EAS CLI (one-time)
npm install -g eas-cli
# Build a development client
eas build --profile development --platform android
eas build --profile development --platform ios
Your eas.json should be configured with the required profiles:
{
"cli": {
"version": ">= 20.4.0",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal"
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}
Coordinate convention
Barikoi tiles and the MapLibre SDK use GeoJSON order: [longitude, latitude].
// ✅ Correct — GeoJSON order
const dhaka: [number, number] = [90.4125, 23.8103];
// ❌ Wrong — Google Maps order (lat, lng)
const wrong: [number, number] = [23.8103, 90.4125];
The expo-location API returns { coords: { latitude, longitude } }, so when converting to a coordinate tuple make sure you swap the order:
const loc = await Location.getCurrentPositionAsync();
const coords: [number, number] = [loc.coords.longitude, loc.coords.latitude];
// ^^^^^^^^^ first ^^^^^^^^ second
Simple map
A minimal screen that renders Barikoi map tiles and centres on the user's location. Copy this into any route file (e.g. app/(tabs)/map.tsx).
The example below stores the API key in component state for runtime configurability. You can alternatively store it under expo.extra.barikoiApiKey in app.json and read it with expo-constants — Constants.expoConfig?.extra?.barikoiApiKey.
import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text, ActivityIndicator, Platform } from 'react-native';
import { Map, Camera, Marker } from '@maplibre/maplibre-react-native';
import * as Location from 'expo-location';
// ── Configuration ───────────────────────────────────────────────────
const BARIKOI_API_KEY = 'YOUR_BARIKOI_API_KEY'; // replace with your key
const MAP_STYLE_URL = `https://map.barikoi.com/styles/osm-liberty/style.json?key=${BARIKOI_API_KEY}`;
const DEFAULT_CENTER: [number, number] = [90.4125, 23.8103]; // Dhaka
const DEFAULT_ZOOM = 12;
// ── Component ───────────────────────────────────────────────────────
export default function SimpleMapScreen() {
const [center, setCenter] = useState<[number, number]>(DEFAULT_CENTER);
const [zoom, setZoom] = useState(DEFAULT_ZOOM);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
(async () => {
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
setError('Location permission denied');
setLoading(false);
return;
}
const location = await Location.getCurrentPositionAsync({
accuracy: Location.Accuracy.Balanced,
});
setCenter([location.coords.longitude, location.coords.latitude]);
setZoom(14);
} catch (err) {
console.warn('Could not get location:', err);
} finally {
setLoading(false);
}
})();
}, []);
if (loading) {
return (
<View style={styles.centered}>
<ActivityIndicator size="large" />
<Text style={styles.loadingText}>Loading map…</Text>
</View>
);
}
return (
<View style={styles.container}>
{error && (
<View style={styles.errorBanner}>
<Text style={styles.errorText}>{error}</Text>
</View>
)}
<Map
style={styles.map}
mapStyle={MAP_STYLE_URL}
logo={false}
attribution={false}
>
<Camera
zoom={zoom}
center={center}
duration={1000}
easing="fly"
/>
<Marker id="center-marker" lngLat={center}>
<View style={styles.marker} />
</Marker>
</Map>
</View>
);
}
// ── Styles ──────────────────────────────────────────────────────────
const styles = StyleSheet.create({
container: {
flex: 1,
},
map: {
...StyleSheet.absoluteFillObject,
},
centered: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 14,
color: '#64748B',
},
errorBanner: {
position: 'absolute',
top: Platform.OS === 'ios' ? 60 : 36,
left: 16,
right: 16,
zIndex: 10,
backgroundColor: 'rgba(239, 68, 68, 0.95)',
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 10,
},
errorText: {
color: '#FFFFFF',
fontSize: 13,
textAlign: 'center',
fontWeight: '500',
},
marker: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: '#007AFF',
borderWidth: 2,
borderColor: '#FFFFFF',
},
});
Utilities reference
Exports from the suggested utils/mapUtils.ts file (see § 4):
| Export | Type | Description |
|---|---|---|
MAP_STYLES | Record<string, (apiKey: string) => string> | Functions that return Barikoi style URLs with the API key appended |
DEFAULT_CENTER | [number, number] | Default map centre — Dhaka [90.4125, 23.8103] |
DEFAULT_ZOOM | number | Initial zoom level (12) |
SELECTED_ZOOM | number | Zoom when a search result is selected (16) |
USER_LOCATION_ZOOM | number | Zoom when centring on user GPS (14) |
CLOSE_ZOOM | number | Zoom for "centre-on-me" button (15) |
FLY_DURATION_MS | number | Camera fly-to animation duration (1000 ms) |
BRAND_BLUE | string | Accent colour for markers and buttons (#007AFF) |
safeParseFloat(value) | (string | number | undefined) => number | Parse a value to float, returning NaN on bad input |
isValidCoordinate(lon, lat) | (number, number) => boolean | Validates lon/lat are finite and within WGS-84 ranges |
haversineDistance(a, b) | ([number,number], [number,number]) => number | Great-circle distance in metres between two [lon, lat] points |
pointInPolygon(point, polygon) | ([number,number], [number,number][]) => boolean | Ray-casting check — is point inside polygon? |
Barikoi API client methods
All methods are available on the object returned by createBarikoiClient({ apiKey }) from the barikoiapis package.
| Method | Parameters | Returns |
|---|---|---|
autocomplete | { q: string, bangla?: boolean } | { data: { places, status } } — matching places with addresses, coordinates, area, city |
reverseGeocode | { longitude, latitude, district?, bangla?, thana?, … } | { data: { place, status } } — address components for a coordinate |
nearby | { longitude, latitude, radius?, limit? } | { data: { places, status } } — POIs within radius (km) |
geocode | { q: string, district?, bangla?, thana? } | { data: { fixed_address, confidence_score_percentage, geocoded_address, status } } |
searchPlace | { q: string } | { data: { places, session_id, status } } — place codes + session ID |
placeDetails | { place_code: string, session_id: string } | { data: { place, session_id, status } } — full details for a place code |
routeOverview | { coordinates: "lon,lat;lon,lat", geometries?, profile? } | { data: { routes, waypoints, code } } — distance, duration, geometry |
calculateRoute | { start: {latitude, longitude}, destination: {latitude, longitude}, type: 'gh', profile? } | { data: { paths, hints, info } } — turn-by-turn instructions, elevation |
snapToRoad | { point: "lat,lon" } | { data: { coordinates, distance, type } } — nearest road point |
checkNearby | { current_latitude, current_longitude, destination_latitude, destination_longitude, radius } | { data: { message, status } } — geo-fence check |
setApiKey(key) | string | void — update the API key at runtime |
getApiKey() | (none) | string — current API key |
See the full barikoiapis README for TypeScript type definitions and detailed parameter docs.
Next steps
- Markers — Use
<Marker id="…" lngLat={[lon, lat]}>to drop custom pins on the map - Current location — Request
Location.requestForegroundPermissionsAsync()and callLocation.getCurrentPositionAsync()to centre the map on the user - Shapes — Render
<GeoJSONSource>and<Layer>for routes, polygons, or GeoJSON overlays - Search & Geocoding — Wire
client.autocomplete()to a<TextInput>with debounce for live search suggestions - Routing — Call
client.routeOverview()orclient.calculateRoute()and render the returned GeoJSON geometry as a<Layer> - Troubleshooting — Common errors and fixes:
- Blank map / 401 error — Verify your API key is valid and has not expired.
- App crashes on launch — Run
npx expo prebuild --cleanand rebuild. Make sure@maplibre/maplibre-react-nativeis inplugins. - Location not working — Ensure
expo-locationis installed and you have requested permissions before callinggetCurrentPositionAsync. - Expo Go shows red screen — Expo Go does not support custom native modules. Use a dev client build instead.