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

Wern Ancheta - Mar 23 '20 - - Dev Community

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:

jscrambler-blog-creating-real-time-tracking-app-nativescript-vue-1

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:

jscrambler-blog-creating-real-time-tracking-app-nativescript-vue-2

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:

jscrambler-blog-creating-real-time-tracking-app-nativescript-vue-3

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:

jscrambler-blog-creating-real-time-tracking-app-nativescript-vue-4

Once you're in the libraries page, search for "Maps SDK", click on Maps SDK for Android and enable it:

jscrambler-blog-creating-real-time-tracking-app-nativescript-vue-5

Next, go to API & Services -> Credentials and click on the Create Credentials button. Then, select API key on the dropdown that shows up:

jscrambler-blog-creating-real-time-tracking-app-nativescript-vue-6

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Next, we need to install the library for generating random strings:

yarn add random-string
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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}`);
    }
});
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

Protect your Vue App with Jscrambler

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>
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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()
};
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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

},
Enter fullscreen mode Exit fullscreen mode

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.");
});
Enter fullscreen mode Exit fullscreen mode

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
    );
},
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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);
},
Enter fullscreen mode Exit fullscreen mode

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);
            });
        });
    });
},
Enter fullscreen mode Exit fullscreen mode

Lastly, add the code for stopping the location tracking:

stopTrackingLocation() {
    this.socket.unsubscribe(`private-${this.trackingID}`);
    this.isTrackingLocation = false;
},
Enter fullscreen mode Exit fullscreen mode

Running the app

At this point, we're ready to run the app. First, start the server:

node server/index.js
Enter fullscreen mode Exit fullscreen mode

Next, expose the server using ngrok:

./ngrok http 5000
Enter fullscreen mode Exit fullscreen mode

Then, update the app/components/Home.vue file with the ngrok URL:

const SERVER_BASE_URL = 'YOUR NGROK HTTPS URL';
Enter fullscreen mode Exit fullscreen mode

You can run the app either on the emulator or a real device:

tns debug android
Enter fullscreen mode Exit fullscreen mode

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.

jscrambler-blog-creating-real-time-tracking-app-nativescript-vue-7

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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .