Using TomTom Maps in React Native: Part 1 - Android
There's no reliable React Native library for TomTom maps. The only way is to implement your own solution using the native SDK. Luckily, TomTom maps has good documentation and an easy to understand SDK. But still, those documents are prepared for those who are using the native environment. Activity, Fragment, Context etc. do not really mean anything to a React Native developer. So, I wanted to write a guide for those who are struggling to use native SDKs for React Native.
Let's start from scratch. First init a new application:
react-native init reactNativeTomTomMap
cd reactNativeTomTomMap
Now run Android Studio and open reactNativeTomTomMap/android folder. For most of the parts, we just need to follow official Android SDK documentation. But since we're developing a React Native app, not a "native" Native app, some parts will be different.
https://developer.tomtom.com/maps-android-sdk/map-initialization
Installation
On the left menu under Gradle Sripts, open build.gradle (Module:app) file and below line under dependencies:
dependencies{
....
implementation("com.tomtom.online:sdk-maps:2.4207")
....
}
TomTom SDK does not exist in the default repository, so we need to tell Android Studio where to find the SDK. Now edit below file:
build.gradle (Project: reactNativeTomTomMap)
allprojects{
...
repositories{
...
maven {
url 'https://maven.tomtom.com:8443/nexus/content/repositories/releases/'
}
...
}
}
If you try to sync gradle, you might get this error:
ERROR: Manifest merger failed : Attribute application@allowBackup value=(false) from AndroidManifest.xml:11:7-34
is also present at [com.tomtom.online:sdk-location:2.4207] AndroidManifest.xml:16:9-35 value=(true).
Suggestion: add 'tools:replace="android:allowBackup"' to element at AndroidManifest.xml:7:5-117 to override.
So, let's do the suggested solution and add tools:replace="android:allowBackup"
to AndroidManifest.xml. But, make sure to add xmlns:tools="http://schemas.android.com/tools"
to manifest element. File will look something like below:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.reactnativetomtommap">
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
tools:replace="android:allowBackup"
>
Now, if you try to sync you might get below error:
Cannot fit requested classes in a single dex file (# methods: 85892 > 65536)
For that, we need to enable multiDex.
Look at https://developer.android.com/studio/build/multidex for more info. Add this to build.gradle(module: app).
android {
....
defaultConfig {
....
multiDexEnabled true
}
....
}
....
dependencies {
....
implementation 'com.android.support:multidex:1.0.3'
....
}
Now it should sync without any errors. But if you run the application, it may fail. multiDex can only be enabled with minSdkVersion is 21 or above. Make sure to make that change:
buildscript {
ext {
....
minSdkVersion = 21
....
}
}
We need to add TomTom API key to AndroidManifest. Create a free account on tomtom.com and generate a new key and add it into AnroidManifest.
<meta-data
android:name="OnlineMaps.Key"
android:value="PASTE_API_KEY_HERE" />
Implementing a Static Native Map
Now let's setup a Native UI module:
Under app > java > com.reactnativetomtommap
create a new package called NativeModules. And under NativeModules, create a file called MapViewManager.java
We will extend it further. But for now let's just implement it as below.
package com.reactnativetomtommap.NativeModules;
import android.view.View;
import android.widget.TextView;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import javax.annotation.Nonnull;
public class MapViewManager extends SimpleViewManager<View> {
@Nonnull
@Override
public String getName() {
return "MapViewManager";
}
@Nonnull
@Override
protected View createViewInstance(@Nonnull ThemedReactContext reactContext) {
TextView tw = new TextView(reactContext);
tw.setText("Map component will be here");
return tw;
}
}
under com.reactnativetomtommap package, create a new file called NativePackage that implements ReactPackage and registers MapViewManager as a module.
package com.reactnativetomtommap;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import com.reactnativetomtommap.NativeModules.MapViewManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class NativePackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new MapViewManager()
);
}
}
My left panel in Android Studio looks like this:
In MainApplication.java make below changes:
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new NativePackage()); // ADD THIS LINE
return packages;
}
In App.js, remove all content and replace it with below:
import React from 'react'
import { View, requireNativeComponent, StyleSheet } from 'react-native'
const TomTomMap = requireNativeComponent('MapViewManager')
const App = () => {
return <>
<TomTomMap style={StyleSheet.absoluteFillObject} />
</>
}
export default App
You should be able to sync and run the application without any errors. You will see a simple text "Map component will be here". Now we are ready to replace that text with a real TomTom map :) Replace createViewInstance method as below and see your success:
@Nonnull
@Override
protected View createViewInstance(@Nonnull ThemedReactContext reactContext) {
MapView mapView = new MapView(reactContext);
return mapView;
}
It's very good so far but it's just a static map. Let's try to control this map from JavaScript as it's the whole point we're using React Native.
Implementing a Dynamic Native Map
Under NativeModules package, create a new class called TomTomMap and extend it from MapView of TomTom library. Instead of the ViewManager, we will use this class to interact with the map itself. ViewManager will be the place where we will manage the bridge between JavaScript and Java.
package com.reactnativetomtommap.NativeModules;
import android.content.Context;
import androidx.annotation.NonNull;
import com.tomtom.online.sdk.map.MapView;
public class TomTomMap extends MapView {
public TomTomMap(@NonNull Context context) {
super(context);
}
}
And edit MapViewManager class as below:
@Nonnull
@Override
protected View createViewInstance(@Nonnull ThemedReactContext reactContext) {
TomTomMap tomTomMap = new TomTomMap(reactContext);
tomTomMap.onResume();
return tomTomMap;
}
Map is still static but we just separated the View from ViewManager. Now is the time to add some @ReactProp. If you're not familiar with adding native UI modules, take a look at the documentation below and come back.
https://facebook.github.io/react-native/docs/native-components-android
Now I want to zoom, set the center of the map and add some markers from JavaScript:
onMapReady(){
alert('Map is ready!')
}
<TomTomMap
onMapReady={this.onMapReady}
markers={[{ lat: 40.9175, lng: 38.3927, label: 'Giresun' }, { lat: 40.9862, lng: 37.8797, label: 'Ordu' }]}
mapZoom={7}
mapCenter={{ lat: 40.9175, lng: 38.3927 }}
style={StyleSheet.absoluteFillObject}
/>
Let's prepare native side for those props:
Edit MapViewManager as below. ViewManager gets the props from JavaScript and passes it to our TomTomMap class extended from MapView. There are 2 types of props. First one is object props which could be a primitive JS object, any JSON object or an array. Second one is the callback props eg. onSomethingReady, onSomethingLoad, onSomethingClick etc. Examine the code below:
package com.reactnativetomtommap.NativeModules;
import android.view.View;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public class MapViewManager extends SimpleViewManager<View> {
@Nonnull
@Override
public String getName() {
return "MapViewManager";
}
@Nonnull
@Override
protected View createViewInstance(@Nonnull ThemedReactContext reactContext) {
TomTomMap tomTomMap = new TomTomMap(reactContext);
tomTomMap.onResume();
return tomTomMap;
}
@ReactProp(name = "mapCenter")
public void setCenter(TomTomMap mapView, @Nullable ReadableMap center) {
mapView.setMapCenter(center);
}
@ReactProp(name = "markers")
public void setMarkers(TomTomMap mapView, @Nullable ReadableArray markers) {
mapView.setMarkers(markers);
}
@ReactProp(name = "mapZoom")
public void setMapZoom(TomTomMap mapView, @Nullable Integer zoom) {
mapView.setMapZoom(zoom);
}
@Nullable
@Override
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.builder()
.put("onMapReady", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onMapReady")))
.build();
}
}
First set of props are gathered using @ReactProp. To read JavaScript's JSON object we need to use Java's ReadableMap and for. Same relationship is between Array and ReadableArray. You cannot set the callbacks using the ReactProp decorator. Every event should be registered as a bubbling event type.
Now edit the TomTomMap class as below and see how it manipulates the the real map object to add markers, zoom and set center.
package com.reactnativetomtommap.NativeModules;
import android.content.Context;
import androidx.annotation.NonNull;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.tomtom.online.sdk.common.location.LatLng;
import com.tomtom.online.sdk.map.CameraPosition;
import com.tomtom.online.sdk.map.MapView;
import com.tomtom.online.sdk.map.MarkerBuilder;
import com.tomtom.online.sdk.map.OnMapReadyCallback;
import com.tomtom.online.sdk.map.SimpleMarkerBalloon;
import com.tomtom.online.sdk.map.TomtomMap;
public class TomTomMap extends MapView implements OnMapReadyCallback {
private int _zoom;
private ReadableArray _markers;
private ReadableMap _center;
private Boolean isMapReady = false;
private TomtomMap _tomTomMap;
public TomTomMap(@NonNull Context context) {
super(context);
this.addOnMapReadyCallback(this);
}
protected void setMapCenter(ReadableMap center) {
_center = center;
if (isMapReady){
_tomTomMap.centerOn(_center.getDouble("lat"), center.getDouble("lng"));
}
}
protected void setMarkers(ReadableArray markers) {
_markers = markers;
if (isMapReady){
_tomTomMap.removeMarkers();
drawMarkers();
}
}
protected void setMapZoom(Integer zoom) {
_zoom = zoom;
if (isMapReady){
_tomTomMap.zoomTo(_zoom);
}
}
@Override
public void onMapReady(@NonNull TomtomMap tomtomMap) {
isMapReady = true;
_tomTomMap = tomtomMap;
reactNativeEvent("onMapReady", null);
LatLng mapCenter = new LatLng(_center.getDouble("lat"), _center.getDouble("lng"));
tomtomMap.centerOn(CameraPosition.builder(mapCenter).zoom(_zoom).build());
drawMarkers();
}
private void drawMarkers(){
for (int i = 0; i < _markers.size(); i++) {
ReadableMap marker = _markers.getMap(i);
SimpleMarkerBalloon balloon = new SimpleMarkerBalloon(marker.getString("label"));
LatLng markerCenter = new LatLng(marker.getDouble("lat"), marker.getDouble("lng"));
_tomTomMap.addMarker(new MarkerBuilder(markerCenter).markerBalloon(balloon));
}
}
protected void reactNativeEvent(String eventName, WritableMap eventParams) {
ReactContext reactContext = (ReactContext) this.getContext();
reactContext
.getJSModule(RCTEventEmitter.class)
.receiveEvent(this.getId(), eventName, eventParams);
}
}
Here, the most important part is the way we implement "onMapReady" delegate. It is an asynchronous process. So, be careful. Because the initial props will be set before the map is ready. Therefore, we used class variables and used them later on when the map became ready. But, onMapReady is called only when map is created or resumed (when app comes from background). But, props could be changed from JavaScript at anytime. For example; setMapZoom will be called at anytime to zoom prop changed in JavaScript. So, if the map is already available; we invoke the relevant functions to manipulate the map.
Now try to change the props in JavaScript side by adding some buttons on the screen. Add your hometown and neighbour city for instance :)
To implement the equivalent solution in IOS. Go ahead for Part 2:
https://cuneyt.aliustaoglu.biz/en/using-tomtom-maps-in-react-native-part-2-ios/