In this tutorial, you're going to learn how to create a real-time location tracking app using the NativeScript Vue template.
Prerequisites
Basic knowledge of NativeScript is required to follow this tutorial. Vue knowledge is optional.
The following package versions were used in creating this tutorial:
- Node 12.7.0
- Yarn 1.19.1
- Nativescript-Vue 2.4.0
- NativeScript CLI 6.1.2
Be sure to install the same versions or higher to ensure that the app will work.
Lastly, you need to have a Pusher and Google account so you can use their API.
App Overview
We're going to create a real-time location tracking app. It will allow the user to either share their current location or track another person's location via a map interface. Here's what it will look like:
You can view the source code on this GitHub repo.
Setting up a Pusher app instance
We need a Pusher app instance in order to use Pusher's services. Go to the Pusher dashboard and create a new Channels app:
Once the app is created, go to the app settings tab and enable client events. This is what will allow the users to trigger real-time events directly from the client-side:
Setting up Google Maps API
To use Google Maps, we need to enable the Google Maps SDK on the Google Cloud Platform console.
On your dashboard, click on the burger menu on the upper left side, hover over APIs & Services and click on Library:
Once you're in the libraries page, search for "Maps SDK", click on Maps SDK for Android and enable it:
Next, go to API & Services -> Credentials and click on the Create Credentials button. Then, select API key on the dropdown that shows up:
That will generate a new API key that you can use later on in the app. Note that you should also restrict access to that key so that it can only be used in the app.
Setting up the project
The app will have both a server and app component. We'll start by setting up the app itself.
Setting up the app
Create a new NativeScript project that uses the Vue template:
tns create LocationTracker --vue
Once that's done, navigate to the newly generated LocationTracker
directory and install the dependencies as below:
tns plugin add nativescript-geolocation
tns plugin add nativescript-google-maps-sdk
tns plugin add nativescript-permissions
tns plugin add nativescript-websockets
tns plugin add pusher-nativescript
Next, we need to install the library for generating random strings:
yarn add random-string
Here's a brief overview of the packages we just installed:
- nativescript-geolocation — used for getting the user's current location.
- nativescript-google-maps-sdk — NativeScript library for working with the Google Maps SDK.
- nativescript-permissions — used for asking permissions in Android.
- nativescript-websockets — WebSocket library for NativeScript. Pusher uses WebSockets so this is a dependency for pusher-nativescript.
- pusher-nativescript — NativeScript library for Pusher integration.
- random-string — for generating random strings that will serve as the unique ID for users who want to share their location.
Once everything is installed, update the app/App_Resources/Android/src/main/AndroidManifest.xml
file. Add the following permissions so we can access the user's current location:
<manifest>
<<!-- ... -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- add these-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
</manifest>
Then, under <application>
, add the <meta-data>
for the Google API key:
<application>
...
<meta-data android:name="com.google.android.geo.API_KEY" android:value="YOUR_GOOGLE_API_KEY" />
</application>
Setting up the server
For the server, create a server
folder inside your working directory and create a package.json
file with the following contents:
{
"name": "ns-realtime-server",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.19.0",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"pusher": "^3.0.0"
}
}
Once that's done, execute yarn
inside the server
folder to install all the packages.
Here's a brief overview of the packages we just installed:
- express — for creating a server.
-
dotenv — allows fetching environment variables (app config) in a
.env
file. - cors — allows the app to make requests to the server.
- body-parser — for parsing the request body to a JavaScript object.
- pusher — for real-time communications.
Building the app
Now, we're ready to build the app. We'll start by adding the server code, then we'll proceed to adding the code for the app itself.
Adding the server code
Create an index.js
file and add the following. This will import all the packages that we need:
const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
require("dotenv").config();
const Pusher = require("pusher");
Next, initialize Pusher. This is what will allow us to connect to the Pusher app instance we created earlier:
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
cluster: process.env.PUSHER_APP_CLUSTER
});
Next, initialize the Express server. Here, we need to enable CORS (Cross-origin resource sharing) so that the app can make a request to the server:
const app = express();
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false })); // disables nested object in the request body
For the server, we'll only need a couple of routes: one for testing if the server is running and the other for authenticating users of the app so that they can trigger messages directly from the client-side:
app.get("/", (req, res) => {
res.send("ok");
});
app.post("/pusher/auth", (req, res) => {
const socketId = req.body.socket_id;
const channel = req.body.channel_name;
const auth = pusher.authenticate(socketId, channel);
res.send(auth);
});
The authentication code above simply authenticates every request that comes in. In a production app, you would need to check in your database if the user who made the request exists.
Lastly, expose the server:
const PORT = 5000;
app.listen(PORT, err => {
if (err) {
console.error(err);
} else {
console.log(`Running on ports ${PORT}`);
}
});
Adding the app code
Now, we're ready to add the code for the app. Start by opening the app/app.js
file and register the MapView
component. We need to do this because, by default, the Google Maps library for NativeScript doesn't support Vue. The code below is used to manually register the MapView
element so we can use it in our templates:
// app/app.js
Vue.registerElement(
"MapView",
() => require("nativescript-google-maps-sdk").MapView
);
Next, open the app/components/Home.vue
file, clear its contents and add the following template. This is going to render the map inside a StackLayout
. Its height is set to 85% so that there will be space for the buttons that the user will use to either share or track location. The MapView
uses the latitude
, longitude
, and zoom
values that we will set later as the data for this component:
<template>
<Page actionBarHidden="true" backgroundSpanUnderStatusBar="false">
<StackLayout height="100%" width="100%" >
<MapView
:latitude="latitude"
:longitude="longitude"
:zoom="zoom"
height="85%"
@mapReady="onMapReady">
</MapView>
<Button text="Stop Sharing Location" @tap="stopSharingLocation" v-if="isSharingLocation"></Button>
<Button text="Share Location" @tap="startSharingLocation" v-else="isSharingLocation"></Button>
<Button text="Stop Tracking Location" @tap="stopTrackingLocation" v-if="isTrackingLocation"></Button>
<Button text="Track Location" @tap="startTrackingLocation" v-else="isTrackingLocation"></Button>
</StackLayout>
</Page>
</template>
Right below the component UI, we add the JavaScript code. Start by importing the packages we need:
import * as geolocation from "nativescript-geolocation";
import * as dialogs from "tns-core-modules/ui/dialogs";
import { Position, Marker } from "nativescript-google-maps-sdk";
import { Accuracy } from "tns-core-modules/ui/enums";
import Pusher from "pusher-nativescript";
const randomString = require("random-string");
Next, add the Pusher app config. Leave the SERVER_BASE_URL
for now — that will have to be an internet-accessible URL. So, we'll use ngrok to expose the local server:
const PUSHER_APP_KEY = "YOUR PUSHER APP KEY";
const PUSHER_APP_CLUSTER = "YOUR PUSHER APP CLUSTER";
const SERVER_BASE_URL = "YOUR PUSHER AUTH SERVER URL";
Next, initialize the data to bind to the component:
export default {
data() {
return {
// current coordinates being displayed on the map
latitude: "",
longitude: "",
zoom: 17, // map zoom level
mapView: null, // map view being rendered
marker: new Marker(), // google map marker
watchID: null, // unique ID for the watch location instance
isSharingLocation: false, // whether the current user is sharing their location or not
isTrackingLocation: false, // whether the current user is tracking someone else's location or not
ownID: null, // unique ID of the current user for tracking
trackingID: null, // unique ID of the person being tracked
socket: null, // pusher socket
ownChannel: null, // current user's own channel for triggering events
trackingChannel: null // channel of the user being tracked by the current user
};
}
// next: add mounted()
};
After this, add the methods to be bind to the component. First is onMapReady()
, which we've attached to the mapReady
event of MapView
. This gets called once the MapView
component is ready for use. args.object
represents the map itself. Assigning it to data bound to the component allows us to manipulate the map later on:
methods: {
onMapReady(args) {
this.mapView = args.object;
}
}
Next, add the mounted()
method. This gets fired when the component is mounted. This is where we generate a unique ID for location sharing. Once that's done, we check if Geolocation (location services) is enabled. If it isn’t, then we request it from the user by calling geolocation.enableLocationRequest()
. If the user enabled it, we proceed to getting their current location and updating the map:
methods: {
onMapReady() {
// ...
}
},
mounted() {
this.ownID = randomString({length: 5}); // unique ID for sharing location
let that = this
geolocation.isEnabled().then(function(isEnabled) {
if (!isEnabled) { // GPS is not enabled
geolocation.enableLocationRequest(true, true).then(() => {
geolocation
.getCurrentLocation({
timeout: 20000
})
.then(location => {
if (!location) {
dialogs.alert('Failed to get location. Please restart the app.');
} else {
// show the user's current location in the map and add the marker
that.updateMap(location);
that.mapView.addMarker(that.marker);
}
});
}, (e) => {
console.log("error: " + (e.message || e));
}).catch(ex => {
console.log("Unable to Enable Location", ex);
});
} else {
// GPS is enabled
geolocation
.getCurrentLocation({
timeout: 20000
})
.then(location => {
if (!location) {
dialogs.alert('Failed to get location. Please restart the app.');
} else {
that.updateMap(location);
that.mapView.addMarker(that.marker);
}
});
}
}, function(e) {
console.log("error: " + (e.message || e));
});
// next: subscribe to own Pusher channel
},
Once that’s done, initialize Pusher and subscribe to the user's own channel. This is where we use the unique ID we generated earlier to subscribe to a private channel. We're using a private channel because we only want authenticated users to use the channel:
this.socket = new Pusher(PUSHER_APP_KEY, {
cluster: PUSHER_APP_CLUSTER,
authEndpoint: `${SERVER_BASE_URL}/pusher/auth`
});
this.ownChannel = this.socket.subscribe(`private-${this.ownID}`);
this.ownChannel.bind("pusher:subscription_error", () => {
dialogs.alert("Failed to connect. Please restart the app.");
});
Below, we have the updateMap()
function. This sets the map coordinates to the location passed as an argument. After that, it also changes the marker position:
updateMap(loc) {
this.latitude = loc.latitude;
this.longitude = loc.longitude;
this.marker.position = Position.positionFromLatLng(
loc.latitude,
loc.longitude
);
},
Next, add the startSharingLocation()
method. This will show the user their unique ID so that they could share it with someone. After that, the app will begin to watch the user's current location via the geolocation.watchLocation()
method. This accepts the success callback as the first argument and the error callback as the second. The third argument is the options.
In this case, we're setting the updateDistance
to 5
meters so that it will only fire the success callback if the change in the distance traveled is 5 meters or more. On the other hand, minimumUpdateTime
is the minimum time interval between each location update. desiredAccuracy
relates to the level of accuracy of the coordinates. Accuracy.high
is the finest location available, so it consumes more battery. When the success callback is fired, it's going to update the map and trigger the client-location-changed
event. The current location is passed to this so whoever subscribes to that same event will get updates in real-time:
methods: {
onMapReady() {
// ..
},
startSharingLocation() {
dialogs.alert(`Your unique ID is: ${this.ownID}`);
this.isSharingLocation = true;
this.watchID = geolocation.watchLocation(
(loc) => {
if (loc) {
this.updateMap(loc);
this.ownChannel.trigger('client-location-changed', {
latitude: loc.latitude,
longitude: loc.longitude
});
}
},
(e) => {
dialogs.alert(e.message);
},
{
updateDistance: 5, // 5 meters
minimumUpdateTime : 5000, // update every 5 seconds
desiredAccuracy: Accuracy.high,
}
);
},
// next: add stopSharingLocation()
}
Next, add the code for stopping the location sharing. This is where we use this.watchID
to stop watching the location:
stopSharingLocation() {
this.isSharingLocation = false;
geolocation.clearWatch(this.watchID);
},
For users who want to track the location of another user, we ask them to enter the unique ID. From there, we simply subscribe to the channel with that ID and bind to client-location-changed
to receive real-time updates:
startTrackingLocation() {
dialogs.prompt("Enter unique ID", "").then((r) => {
this.trackingID = r.text;
this.isTrackingLocation = true;
this.trackingChannel = this.socket.subscribe(`private-${this.trackingID}`);
this.trackingChannel.bind('pusher:subscription_succeeded', () => {
this.trackingChannel.bind('client-location-changed', (loc) => {
this.updateMap(loc);
});
});
});
},
Lastly, add the code for stopping the location tracking:
stopTrackingLocation() {
this.socket.unsubscribe(`private-${this.trackingID}`);
this.isTrackingLocation = false;
},
Running the app
At this point, we're ready to run the app. First, start the server:
node server/index.js
Next, expose the server using ngrok:
./ngrok http 5000
Then, update the app/components/Home.vue
file with the ngrok URL:
const SERVER_BASE_URL = 'YOUR NGROK HTTPS URL';
You can run the app either on the emulator or a real device:
tns debug android
However, since the nature of the app requires us to change locations, it's easier if you use an emulator for testing. This way, you can easily change the location by searching for a specific location or pointing to a specific location via a map interface. The Genymotion emulator allows you to do it very easily.
Alternatively, you can also use a fake location app such as Floater on your Android device. This will allow you to spoof the current location and specify a different one via a map interface — though I've had an issue with this method. It seems that it switches the current location and the fake location back and forth, which beats the whole purpose because you can't properly test the functionality.
Conclusion
That's it! In this tutorial, you learned how to create a real-time location tracking app in NativeScript.
Along the way, you learned how to work with the NativeScript Vue template, rendering Google Maps, watching the user's current location, and publishing it in real-time.
As always, we recommend that you protect your JavaScript source code when you're developing commercial or enterprise apps. See our tutorials on protecting Vue and NativeScript.