Experience seamless collaboration and exceptional results.
Before we jump into the details of the update to react-native-mapbox-navigation, it's worth mentioning an interesting observation. Mapbox doesn't offer native support for their Navigation SDK in React Native. Why is that? We can only speculate, but the React Native ecosystem is vast and constantly evolving, and it might not always align perfectly with the native SDK development cycle.
The journey begins with Android. The android/build.gradle file was a relic of an older React Native era. We needed to bring it up to date. Here's what changed:
classpath "com.android.tools.build:gradle:7.2.2"
kotlinVersion
within the ext
extension of the root project using rootProject.ext.has("kotlinVersion")
. rootProject.ext.get("kotlinVersion")
. kotlinVersion
property is not present in the root project's ext
extension, the configuration defaults to using a version specified within the project’s own properties. "MapboxNavigation_kotlinVersion"
.gradle.properties
file of the project. 1.6.0
, denoted by the entry MapboxNavigation_kotlinVersion=1.6.0
. MapboxNavigation_compileSdkVersion=33
), minimum SDK version (MapboxNavigation_minSdkVersion=21
), and target SDK version (MapboxNavigation_targetSdkVersion=33
) are also declared within the same gradle.properties
file. gradle.properties
MapboxNavigation_kotlinVersion=1.6.0
MapboxNavigation_compileSdkVersion=33
MapboxNavigation_minSdkVersion=21
MapboxNavigation_targetSdkVersion=33
Gradle Wrapper Version Update
We've updated our project's Gradle Wrapper to version 8.2. The gradle-wrapper.properties
file now specifies:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
Adding Waypoints: A Kotlin Challenge
An additional update to the Android functionality includes the incorporation of waypoints into navigation routes. This feature allows users to insert intermediate stops within their route. To accommodate this new capability, several adjustments were made to the MapboxNavigationManager.kt
file:
waypoints
property, which is capable of receiving an array of coordinates.Point
objects, each defined by latitude and longitude values.MapboxNavigationManager.kt:
/**
* Sets waypoints for the MapboxNavigationView based on a React prop.
* This function converts a ReadableArray of waypoint coordinates into a list of Point objects,
* then updates the MapboxNavigationView with these waypoints.
*
* @param view The MapboxNavigationView instance on which waypoints are to be set.
* @param waypointsArray The array of waypoints received from React, where each waypoint
* is expected to be an array of two numbers: [longitude, latitude].
*/
@ReactProp(name = "waypoints")
fun setWaypoints(view: MapboxNavigationView, waypointsArray: ReadableArray?) {
// Check if the waypointsArray is not null to proceed
waypointsArray?.let {
// Initialize a mutable list to hold the converted Point objects
val waypoints = mutableListOf<Point>()
// Iterate over each item in the waypointsArray
for (i in 0 until it.size()) {
// Attempt to get the array (longitude, latitude) for the current waypoint
val waypointArray = it.getArray(i)
// Check if the waypointArray is not null and contains at least two elements
if (waypointArray !== null && waypointArray.size() >= 2) {
// Extract longitude and latitude values
val longitude = waypointArray.getDouble(0)
val latitude = waypointArray.getDouble(1)
// Create a Point object from the longitude and latitude and add it to the waypoints list
waypoints.add(Point.fromLngLat(longitude, latitude))
}
}
// Update the MapboxNavigationView with the list of waypoints
view.setWaypoints(waypoints)
}
}
MapboxNavigationView.kt:
// Defining a variable to hold waypoints
private var waypoints: List<Point>? = null
// Starts the routing process
private fun startRoute() {
// Registering necessary event listeners for navigation updates
mapboxNavigation.registerRoutesObserver(routesObserver)
mapboxNavigation.registerArrivalObserver(arrivalObserver)
mapboxNavigation.registerRouteProgressObserver(routeProgressObserver)
mapboxNavigation.registerLocationObserver(locationObserver)
mapboxNavigation.registerVoiceInstructionsObserver(voiceInstructionsObserver)
mapboxNavigation.registerRouteProgressObserver(replayProgressObserver)
// Creating a list of coordinates for the route that includes the origin, destination, and any waypoints
val coordinatesList = mutableListOf<Point>()
// Adding origin to the list if it's not null
this.origin?.let { coordinatesList.add(it) }
// Adding all waypoints to the list if they exist
this.waypoints?.let { coordinatesList.addAll(it) }
// Adding destination to the list if it's not null
this.destination?.let { coordinatesList.add(it) }
// Finding a route with the specified coordinates
findRoute(coordinatesList)
}
// Function to find a route based on a list of coordinates
private fun findRoute(coordinates: List<Point>) {
try {
// Building route options with the desired parameters
val routeOptionsBuilder = RouteOptions.builder()
.applyDefaultNavigationOptions()
.applyLanguageAndVoiceUnitOptions(context)
.coordinatesList(coordinates) // Setting the list of coordinates (origin, waypoints, destination)
.profile(DirectionsCriteria.PROFILE_DRIVING) // Setting the routing profile to driving
.steps(true) // Including step-by-step instructions in the route
// Optionally setting maximum height and width if provided
maxHeight?.let { routeOptionsBuilder.maxHeight(it) }
maxWidth?.let { routeOptionsBuilder.maxWidth(it) }
// Building the final route options
val routeOptions = routeOptionsBuilder.build()
// Requesting routes with the specified options
mapboxNavigation.requestRoutes(
routeOptions,
object : RouterCallback {
override fun onRoutesReady(
routes: List<DirectionsRoute>,
routerOrigin: RouterOrigin
) {
// Handling successful route finding
setRouteAndStartNavigation(routes)
}
override fun onFailure(
reasons: List<RouterFailure>,
routeOptions: RouteOptions
) {
// Handling route finding failure
sendErrorToReact("Error finding route $reasons")
}
override fun onCanceled(routeOptions: RouteOptions, routerOrigin: RouterOrigin) {
// Handling route request cancellation (no implementation needed here)
}
}
)
} catch (ex: Exception) {
// Handling any exceptions during route finding
sendErrorToReact(ex.toString())
}
}
Android NDK Compatibility: A Tricky Scenario
One thing to note in Android is the compatibility challenge between React Native and the Mapbox Navigation SDK. At the time of this update, React Native version 0.72.6 uses NDK version 23, while the Mapbox Navigation SDK supports only up to NDK version 21. This creates a bit of a conundrum.
Developers have two options:
(or)
When it comes to iOS, the journey was surprisingly smoother. The react-native-mapbox-navigation.podspec file underwent some key updates:
React-native-mapbox-navigation.podspec:
Experience seamless collaboration and exceptional results.
//11.0 -> 12.4
s.platforms = { :ios => "12.4" }
//2.1.1 -> 2.17.0
s.dependency "MapboxNavigation", "~> 2.17.0"
The iOS update also brought waypoint support to MapboxNavigationView.swift. This was achieved with relative ease:
ios/MapboxNavigationManager.m:
/**
* Exports the `waypoints` property to React Native, allowing it to be set from the JavaScript side.
* This property is expected to be an array of arrays, where each inner array represents a waypoint
* consisting of two elements: longitude and latitude (in that order).
*
* By defining this property, we enable the React Native app to dynamically specify waypoints for navigation
* routes directly from JavaScript, enhancing the flexibility of route creation and modification.
*
* Example JavaScript usage:
* <MapboxNavigationView waypoints={[[longitude1, latitude1], [longitude2, latitude2]]} />
*/
RCT_EXPORT_VIEW_PROPERTY(waypoints, NSArray<NSArray>)
ios/MapboxNavigationManager.m:
// Property to store waypoints received from React Native. It triggers layout update on change.
@objc var waypoints: NSArray = [] {
didSet { setNeedsLayout() }
}
// Embeds the navigation view into the current view, setting up the route with waypoints.
private func embed() {
// Ensuring origin and destination coordinates are correctly set
guard origin.count == 2 && destination.count == 2 else { return }
embedding = true
// Creating waypoints for origin and destination
let originWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: origin[1] as! CLLocationDegrees, longitude: origin[0] as! CLLocationDegrees))
let destinationWaypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: destination[1] as! CLLocationDegrees, longitude: destination[0] as! CLLocationDegrees))
// Initializing the waypoints array with origin to start building the complete route
var waypointsArray = [originWaypoint]
// Looping through any intermediate waypoints provided and adding them to the waypoints array
for waypointArray in waypoints {
if let waypointCoordinates = waypointArray as? NSArray, waypointCoordinates.count == 2,
let lat = waypointCoordinates[1] as? CLLocationDegrees, let lon = waypointCoordinates[0] as? CLLocationDegrees {
let waypoint = Waypoint(coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon))
waypointsArray.append(waypoint)
}
}
// Appending destination waypoint to the array
waypointsArray.append(destinationWaypoint)
// Creating navigation options with the waypoints array, setting the profile to driving while avoiding traffic
let options = NavigationRouteOptions(waypoints: waypointsArray, profileIdentifier: .automobileAvoidingTraffic)
// Requesting route calculation with the given options
Directions.shared.calculate(options) { [weak self] (_, result) in
guard let strongSelf = self, let parentVC = strongSelf.parentViewController else {
return
}
switch result {
case .failure(let error):
// Handling route calculation failure
strongSelf.onError!(["message": error.localizedDescription])
case .success(let response):
// Proceeding with navigation setup upon successful route calculation
guard let weakSelf = self else {
return
}
// Setting up the navigation service and navigation options
let navigationService = MapboxNavigationService(routeResponse: response, routeIndex: 0, routeOptions: options, simulating: strongSelf.shouldSimulateRoute ? .always : .never)
let navigationOptions = NavigationOptions(navigationService: navigationService)
// Creating and configuring the NavigationViewController
let vc = NavigationViewController(for: response, routeIndex: 0, routeOptions: options, navigationOptions: navigationOptions)
vc.showsEndOfRouteFeedback = strongSelf.showsEndOfRouteFeedback
StatusView.appearance().isHidden = strongSelf.hideStatusView
NavigationSettings.shared.voiceMuted = strongSelf.mute;
vc.delegate = strongSelf
// Embedding the navigation view controller into the current view hierarchy
parentVC.addChild(vc)
strongSelf.addSubview(vc.view)
vc.view.frame = strongSelf.bounds
vc.didMove(toParent: parentVC)
strongSelf.navViewController = vc
}
// Updating state flags after embedding
strongSelf.embedding = false
strongSelf.embedded = true
}
}
src/typings.ts:
//add waypoints type
waypoints?: Coordinate[];
dist/typings.d.ts:
//add waypoints into IMapboxNavigationProps
waypoints?: Coordinate[];
How to Install My Forked Version
All the changes and enhancements I've made to react-native-mapbox-navigation are available in my forked repository on GitHub. If you want to use my version, follow these steps to install it:
Uninstall the Original Package (if already installed)
npm uninstall react-native-mapbox-navigation
# or
yarn remove react-native-mapbox-navigation
Install My Forked Version:
npm install https://github.com/sarafhbk/react-native-mapbox-navigation
# or
yarn add https://github.com/sarafhbk/react-native-mapbox-navigation
As a non-native developer, this update was a learning curve. Understanding Gradle, Kotlin, Podspec, and Swift was like deciphering a complex code. That's where ChatGPT came to the rescue, offering guidance and solutions when navigating these uncharted territories.
As a result, react-native-mapbox-navigation has been revitalized. It's now updated with new tools, improved features, and the ability to include waypoints. This upgrade means developers can build better and more personalized navigation experiences in their React Native apps.
Frequently Asked Questions
Q1: Why should I use your forked version of react-native-mapbox-navigation?
My forked version includes several enhancements and updates that improve the functionality and flexibility of react-native-mapbox-navigation. If you want access to the latest features and improvements, my version is a great choice.
Q2: What's the difference between your version and the original package?
My version includes updates like support for waypoints, compatibility enhancements, and bug fixes. You can refer to my repository's README or documentation for a detailed list of changes.
Q3: Do I need to make any additional configuration changes?
In most cases, the installation process should be straightforward. However, depending on your project's requirements, you may need to make specific configuration changes, which I'll guide in the README of my repository.
Q4: How can I report issues or contribute to your forked repository?
I encourage developers to contribute and report issues. You can visit the issues page of my GitHub repository to report problems or provide feedback. If you'd like to contribute, feel free to create pull requests.