Cross-Platform mobile applications with NativeScript Core and TypeScript – Part 2

In the previous part of this tutorial, we learned how to create a simple NativeScript Core application with the support of TypeScript, and how to run the preview by using NativeScript Playground. Also, we have seen how to build a native application by using the NativeScript command-line interface (CLI). Now, we are going to exploit some of the most important features of this framework, through the study of a simple example available in this ready-to-use code repository.

To speed up the development of your application, I suggest you install NativeScript Sidekick. In this way, you will be able to build and deploy your application directly on your phone, keeping the code changes synchronised without using any command line. Done? Let’s get started!

Wait… I want to see it in action before!

Sure! First of all, you need to set some parameters. From the root folder of the repository, open the app/environments/environment.*.ts (production and debug) file and set the following values:

  • tokenEndPoint: http://YOUR_LOCAL_IP:3000/token
  • socketURL: http://YOUR_LOCAL_IP:3001
  • apiPath: http://YOUR_LOCAL_IP:3000/api

Instead of the “YOUR_LOCAL_IP” placeholder, use your local IP. On a Microsoft Windows system, you can obtain it by using the following command inside a command prompt:

ipconfig

# Console output
# [...]
# Address IPv4. . . . . . . . . . . . : 192.168.1.6 (Your local IP)
# [...]

In order to work, the device (smartphone or tablet) that will run the NativeScript application will need to be connected to the same network as your computer. Also, if you have a firewall installed on your computer, you have to open the following ports:

  • Port 3000 – TCP Inbound
  • Port 3001 – TCP Inbound

Follow this tutorial on how to open a port in Windows Firewall.

Now you need to open NativeScript Sidekick and to click on the “Open App” button. Choose the root folder of the repository and plug your phone into the computer, then you will be able to click on the “Run on Device” button. You will need to wait a few minutes for the first build of the application, so take a coffee break and then come back to see the loading splash screen on your phone! 😉

The purpose of this simple application is to test some of the most interesting features provided by the NativeScript framework, which are commonly used during the development of mobile software. To play with it, you need to run the back-end server, included inside the server folder of the repository. Therefore, If you haven’t done it yet, download and install Node.js LTS, including the npm packages manager. Once the setup is completed, by using the Visual Studio Code terminal, run the following commands inside the server folder:

npm install
npm start

Note. The server application has been developed as a passthrough tool for this example client. Don’t use(!) it as a basis for a production back-end, because there are a lot of hard-coded values!

Let’s play!

The first available view of the application is the login page. Starting from here, we are going to analyse the exploited features one by one, to understand how they work.

Toggle Menu

Tap on the hamburger button at the upper-left side of the page to see the toggle menu in action. Here, the nativescript-ui-sidedrawer plugin allows managing this UI component in a very simple way.

Inside the body-page.xml file, the <sd:RadSideDrawer> component allows defining the <sd:RadSideDrawer.drawerContent> tag, which corresponds to the set of <Button> and <Label> present inside the left menu. Contrariwise, the <sd:RadSideDrawer.mainContent> tag contains the current page, dynamically rendered by the <Frame> component.

<sd:RadSideDrawer id="sideDrawer">
    <sd:RadSideDrawer.drawerContent>
        <!-- Left menu content here -->
    </sd:RadSideDrawer.drawerContent>
    <sd:RadSideDrawer.mainContent>
        <!-- Dynamic render page -->
        <Frame defaultPage="{{ dynamicPage }}">
        </Frame>
    </sd:RadSideDrawer.mainContent>
</sd:RadSideDrawer>

In this file, you can also see how the <Repeater> component works, which allows for the repetition of its content as many times as defined inside the items repeat condition, like the Angular NgForOf component.

<!-- Repeat the <Label> component for each 'items' item -->
<Repeater items="{{ items }}">
    <Repeater.itemTemplate>
        <!-- The label text corresponds to the 'item' value -->
        <Label text="{{ $value }}" />
    </Repeater.itemTemplate>
</Repeater>

NativeScript doesn’t provide a directive that allows rendering a template depending on particular conditions, like the Angular NgIf component. Instead, it uses the visibility mechanism provided by the view tags.

<!-- Show the label if the 'visible' property is true -->
<Label visibility="{{ visible? 'visible' : 'collapse' }}" />

Check out this document or take a look at the examples inside the body page to get more details. Finally, inside the body-view-model.ts file, you can find the methods and the properties bound to each button of the left menu.

The toggle menu action is called from the header page, through the Global Event Dispatcher. Once the toggle menu Lite Event is triggered, the toggleDrawerState function is executed, expanding the toggle menu.

// How to listen the toggle menu global event
this.globalEventsDispatcher.listenEvent(EVENTS.TOGGLE_MENU, () => {
    this._drawer.toggleDrawerState();
});

// How to trigger the toggle menu global event
this.globalEventsDispatcher.triggerEvent(EVENTS.TOGGLE_MENU);

In the next part of this tutorial, we will see in detail how the Global Event Dispatcher works.

Login / Logout

Inside the login-page.xml file, you can check out how the nativescript-ui-dataform plugin allows you to implement a validation form for the user input, through the <df:RadDataForm> component. To do this, you just need to define a <df:EntityProperty> component for each input field to validate, together the <df:EntityProperty.validators> component, which describes the validation rules.

<!-- The 'form' model contains the fields to validate  -->
<df:RadDataForm id="formId" source="{{ form }}" >
    <df:RadDataForm.properties>
        <!-- The 'name' field can not be empty  -->
        <df:EntityProperty name="name" displayName="name" index="0" >
            <df:EntityProperty.validators>
                <df:NonEmptyValidator />
            </df:EntityProperty.validators>
        </df:EntityProperty>
    </df:RadDataForm.properties>
</df:RadDataForm>

Inside the login-view-model.ts file, the onLoginButtonTap function allows the validation of the data form through the validateAll method. Then, LoginService and IdentityService are called. The first one is used to authenticate the user, while the second one is used to authorise him to use specific resources. In the next part of this tutorial, we will see in detail how the authentication and authorisation processes work.

To log in and proceed to the home page, you have to use “guest” as username and password. Once logged in, the logout button and the username will become visible on the left menu. The following line of codes allows us to navigate to the home page:

async onLoginButtonTap(args: EventData) : Promise<void> {
    const button: Button = <Button>args.object;
    const page: Page = button.page;
    // Validate the data form
    // Authenticate and authorise the user
    // Navigate to the home page
    page.frame.navigate("/pages/home/home-page");
}

Home Page

Inside the home page, a set of buttons will allow you to test most of the exploited features, with just one click!

Here, you can see an example of text localisation. The buttons names are localised for the English and Italian languages. As you know, the value of a localised text depends on the default language of the target operating system. Thanks to the nativescript-localize plugin, the implementation of this feature is very simple!

By taking a look at the home-page.xml file, it’s possible to see how the translation function works:

<!-- The button text is now localised -->
<Button text="{{ L('keyword') }}" />

This function looks for the “keyword” string, inside the app/resources/i18n/YOUR_LANGUAGE/strings.xml file. If found, the associated translated text will be shown:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- We have defined the "keyword" string -->
    <string name="keyword">Hello!</string>
</resources>

Therefore, for each target language, you need to create a “YOUR_LANGUAGE” folder by using this nomenclature (check out the “ISO 639-1 Code” column). During the execution, the localisation plugin will automatically check the target operating system and will use the corresponding language.

Dialogs

From the home page, click on the “Test Dialog” button. By looking at the onTestDialogButtonTap function, inside the home-view-model.ts file, it’s possible to see how to use the dialogs module to show a dialog message:

await dialogs.alert({
    title: "Hello!",
    message: "It's dialog here!",
    okButtonText: "OK"
});

Toasts

From the home page, click on the “Test Toast” button. By looking at the onTestToastButtonTap function, inside the home-view-model.ts file, it’s possible to see how to use the nativescript-toasty plugin to show a toast message:

const toast = new Toasty({ text: 'Toast message' });
toast.show();

Notifications

From the home page, click on the “Test Notification” button. By looking at the onTestNotificationButtonTap function, inside the home-view-model.ts file, it’s possible to see how to use the nativescript-local-notifications plugin to show a notification message:

// We need to obtain some permissions
var hasPermission = await LocalNotifications.hasPermission();
if (hasPermission) {
    // Now we can schedule our notification
    var scheduled = await LocalNotifications.schedule([{
        id: 1,
        title: 'Title',
        body: 'Body',
        groupedMessages: ["First", "Second..."],
        groupSummary: "Group summary"
    }]);
}

As you can see, the notifications plugin needs to obtain some permissions to work. Once obtained, you can use the schedule function to customise and to show your notifications.

Accelerometer

From the home page, click on the “Test Accelerometer” button. By looking at the onTestAccelerometerButtonTap function, inside the home-view-model.ts file, it’s possible to see how to use the nativescript-accelerometer plugin, through the methods provided by the AccelerometerManager class:

this.accelerometerManager.startDataReading((data) => {
    // Data available on: data.x, data.y, data.z
});

The AccelerometerManager class exposes the startDataReading and stopDataReading functions, which respectively allow us to start and to stop the data reading from the accelerometer. Take a look at the accelerometer-manager.ts file to get more details.

GPS

From the home page, click on the “Test GPS” button. By looking at the onTestGPSButtonTap function, inside the home-view-model.ts file, it’s possible to see how to use the nativescript-geolocation plugin, through the methods provided by the GPSManager class:

// We need to obtain some permissions
var hasPermission = await this.gpsManager.getPermissions();
if (hasPermission) {
    // Now we can get the current location
    var location = await this.gpsManager.getLocation();
    // Data available on: location.latitude, location.longitude
}

As you can see, the GPS plugin needs to obtain some permissions to work. Once obtained, you can use the getLocation function to get the latitude and the longitude of the current location. Take a look at the gps-manager.ts file to get more details.

Camera

From the home page, click on the “Test Camera” button. By looking at the onTestCameraButtonTap function, inside the home-view-model.ts file, it’s possible to see how to use the nativescript-camera-plus plugin, through the methods provided by the CameraManager class:

// Parameters initialisation
var camOpts = {height: 400, saveToGallery: false};
var chooseOpts = {showImages: true, showVideos: true, height: 300};
var camView = this._page.getViewById('camPlus');

// Camera initialisation
this.cameraEnabled = true;
this.cameraHeight = camOpts.height;
this.cameraManager.initCamera(camView, camOpts, chooseOpts, false);

// Now we need to obtain some permissions...

The initCamera method allows initialising the camera through some configuration parameters. Once initialised, the camera needs to obtain some permissions to work. Depending on the needed features, you can get the corresponding permissions by using one or more of the following functions:

// Camera permissions
await this.cameraManager.getCameraPermissions();
// Video recording permissions
this.cameraManager.getVideoRecordingPermissions();
// Audio recording permissions
await this.cameraManager.getAudioPermissions();
// Photo and video storage permissions
await this.cameraManager.getStoragePermissions();

Also, the CameraManager class provides a list of interceptable events, which are fired during the camera usage. In the next part of this tutorial, we will see in detail how to listen and to trigger a Lite Event.

// Errors during the camera initialisation
onError = new LiteEvent<any>();
// The camera has been toggled
onCameraToggle = new LiteEvent<any>();
// A new photo has been taken
onPhotoCaptured = new LiteEvent<any>();
// An image has been selected from the gallery
onImageSelected = new LiteEvent<any>();
// The camera is ready to record a video
onVideoRecordingReady = new LiteEvent<any>();
// The camera is recording a new video
onVideoRecordingStarted = new LiteEvent<any>();
// A new video has been recorded
onVideoRecordingFinished = new LiteEvent<any>();

After obtaining the required permissions, it’s possible to exploit the available camera functions through the following methods:

// Start the video recording
await this.cameraManager.startRecordingVideo();
// Stop the video recording
await this.cameraManager.stopRecordingVideo();
// Take a photo
await this.cameraManager.takePhoto();
// Switch On-Off the flash
await this.cameraManager.toggleFlash();
// Switch the camera (Front or Rear)
await this.cameraManager.toggleCamera();
// Open the media gallery
await this.cameraManager.openGallery();

Socket

From the home page, click on the “Test Socket” button. By looking at the onTestSocketButtonTap function, inside the home-view-model.ts file, it’s possible to see how to use the nativescript-socketio plugin, through the methods provided by the SocketManager class:

this.socketManager.onMessage.on((payload) => {
    // A new message from 'payload.from' with the 'payload.message' content
});

this.socketManager.onConnect.on((payload) => {
    // Connection successful
});

this.socketManager.onUsersList.on((users) => {
    // You have received the list of all connected users
    // Send a message to the first one (ourselves)
    this.socketManager.sendMessage(users[0].socketId, "Hello!");
});

// Connect to the socket
this.socketManager.connect(environment.current.socketURL);

Note. As you can see, inside the list of all users, the first one corresponds to ourselves. This happens only for test purpose!

Once connected to the socket server by using the connect method, it’s possible to send a message to a specific user by using the sendMessage function, or to receive a message by intercepting the onMessage event. Even the SocketManager class provides a list of interceptable events, which are fired during the socket connection:

// The user is connected to the socket
onConnect = new LiteEvent<any>();
// The user has been reconnected to the socket
onReconnect = new LiteEvent<any>();
// The user has been disconnected from the socket
onDisconnect = new LiteEvent<any>();
// A new message is available
onMessage = new LiteEvent<any>();
// Errors during the socket connection
onError = new LiteEvent<any>();
// The list of all connected users is available
onUsersList = new LiteEvent<SocketUser[]>();
// A new user has been connected (also available inside the list)
onUserConnected = new LiteEvent<SocketUser>();
// A user has been disconnected (no longer available inside the list)
onUserDisconnected = new LiteEvent<SocketUser>();

Worker

From the home page, click on the “Test Worker” button. By looking at the onTestWorkerButtonTap function, inside the home-view-model.ts file, it’s possible to see how to use the worker-loader plugin and to exploit the multi-threading system, through the methods provided by the Worker class:

const worker = new TestWorker();

// We are ready to read a message from the worker
worker.onmessage = message => {
    // Content available on 'message.data'
};

// Sending a message to the worker
worker.postMessage("Hello from Worker!");

A worker allows running one or more different instructions on a separate thread. From the main thread, it’s possible to send data (by copy) to a running worker by using the postMessage method and then, to wait to receive an asynchronous response through the onmessage callback. The worker body has to be defined in another file. Here below is the definition of the test-worker.cs file, used in this example:

import "globals";

const context: Worker = self as any;

context.onmessage = message => {
    setTimeout(() => {
        (<any>global).postMessage("Message received: " + message.data + "");
    }, 500)
};

The postMessage method allows sending the result of the execution back to the main thread, invoking the onmessage callback as a consequence.

That’s all for now!
In the third part of this tutorial, we are going to analyse in detail the infrastructure of the example application, to understand how to use the main components of this framework and the support structures.

Happy coding!