Creating a Real-Time Location Tracking App with NativeScript-Vue

By Júlia Rocha 11 min read

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

Install the same version or higher to ensure the app works.

Lastly, you need a Pusher and Google account to use their API.

App Overview

We’re going to create a real-time location-tracking app. It will allow users to share their current location or track another person’s location via a map interface. Here’s what it will look like:
overview-after-creating-a-real-time-location-tracking-app
You can view the source code in the GitHub repo.

Setting up a Pusher app instance

We need a Pusher app instance to use Pusher’s services. Go to the Pusher dashboard and create a new Channels app:
creating-the-channels-app-example
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:
enable-client-events-on-app-settings-explanation

Setting up Google Maps API

Setting up the Google Maps API requires enabling the Google Maps SDK on the Google Cloud Platform console.

On your dashboard, click on the hamburger menu on the upper left side, hover over APIs and Services, and click on Library.

Once you’re on the libraries page, search for Maps SDK, click on Maps SDK for Android, and enable it:
enable-maps-SK-for-AndroidGo to API & Services -> Credentials and click on the Create Credentials button. Then, select the API key from the dropdown that shows up:
selecting-the-APIThat will generate a new API key you can use later in the app. Note that you should restrict access to that key so it can only be used in the app.

Setting up the project

The app will have both a server and an 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:

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 into a JavaScript object.
  • pusher: for real-time communication.

Building the app

Now, we’re ready to build the app. We’ll start by adding the server code, then add the app code.

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 app code. Start by opening the app/app.js file and registering the MapView component.

We do this because, by default, the NativeScript Google Maps library 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. Set its height to 85% so there’s space for the buttons the user will use to share or track their 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 configuration. Leave SERVER_BASE_URL for now; it needs 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 be bound 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()
};

Next, add the methods to 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 a data property allows us to manipulate the map later.

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.

Next, we check whether 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 get their current location and update 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 use a private channel because we only want authenticated users to access it.

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 is 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 shows the user their unique ID so they can share it with someone.

After that, the app starts watching the user’s current location via 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 set updateDistance to 5 meters so it fires the success callback only when the distance traveled changes by 5 meters or more.

On the other hand, minimumUpdateTime is the minimum interval between location updates. desiredAccuracy refers 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 code to stop 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 another user’s location, ask them to enter a 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 app requires us to change locations, it’s easier to use an emulator for testing. This way, you can easily change the location by searching for a specific location or pointing to one on a map. The Genymotion emulator makes this easy.

Alternatively, you can use a fake location app, such as Floater, on your Android device.

This lets you spoof your current location and set a different one via a map interface—though I’ve had an issue with this method. It seems to switch the current location and the fake location back and forth, which defeats the 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 to work with the NativeScript Vue template, render Google Maps, track the user’s current location, and publish 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.