In the last article, I introduced you to NativeScript. There you learned the basics of NativeScript and how it differs from other mobile development frameworks. This time you'll be getting your hands dirty by creating your first NativeScript app. I'll walk you through the whole process of building an app with NativeScript, from setting up your development environment to running the app on your device. Here's an outline of what I'll discuss:
- Setting up NativeScript
- Building the app
- Running the app
- Debugging the app
We'll be specifically running the app on the Android platform. But you can still follow along if you want to deploy to iOS as the code will be pretty much the same. The only differences are in the process for setting up NativeScript and the commands that you execute when running the app.
The completed source code for this app is available from the tutorial GitHub repo.
1. Setting Up NativeScript
To set up NativeScript, you first have to install Node.js. Once Node.js is installed, you can install the NativeScript command-line tool by executing npm install -g nativescript
on your terminal.
The final step is to install the development tool for each platform that you want to deploy to. For Android, this is the Android SDK. For iOS, it's XCode. You can follow the installation guide on the NativeScript website for more detailed instructions on how to set up the necessary software for your development environment.
Once you've set up your environment, execute tns doctor
to make sure that your environment is ready for NativeScript development. If you're on Linux or Windows, you'll see something like this if your environment is ready:
NOTE: You can develop for iOS only on Mac OS X systems. To be able to work with iOS devices and projects, you need Mac OS X Mavericks or later. Your components are up-to-date. No issues were detected.
There's a note in there that you can only develop for iOS only on Mac OS X systems. This means that if you're on a PC, you'll only be able to deploy to Android devices. However, if you're on Mac, you'll be able to deploy on both iOS and Android platforms.
If you run into any problems during the setup, you can get an invitation to join the NativeScript Slack Community and once you have joined, go to the getting started channel and ask your questions in there.
2. Creating the App
The app that we're going to build is a note-taking app. It will allow the user to create notes, each with an optional image attachment that will be captured with the device camera. The notes are persisted using NativeScript application settings, and can be individually deleted.
Here's what the app is going to look like:
Start by executing the following command to create a new NativeScript project:
tns create noter --appid "com.yourname.noter"
noter
is the name of the project, and com.yourname.noter
is the unique app ID. This will be used later on to identify your app once you submit it to the Play or App Store. By default, the tns create
command will create the following folders and files for you:
- app
- node_modules
- platforms
- package.json
You'll typically only have to touch the files inside the app directory. But there are also instances where you might need to edit files inside the platforms/android directory. One such case is when a plugin that you're trying to use doesn't automatically link the dependencies and assets that it needs.
Next, navigate to the app directory and delete all files except the App_Resources folder. Then create the following files:
- app.js
- app.css
- notes-page.js
- notes-page.xml
These are the files that will be used by the NativeScript runtime. Just like when building web pages, .css files are used for styling, and .js files for functionality. But for the markup of the app, we use XML instead of HTML. Usually you would create a separate folder for each screen of the app (e.g. login, sign up, or dashboard) and have XML, CSS, and JavaScript files inside each folder. But since this app has only one screen, we created all the files inside the root directory.
If you need more information about the NativeScript directory structure, check out Chapter 2 of the NativeScript Getting Started Guide.
3. The Entry Point File
Open the app.js file and add the following code:
var application = require("application"); application.start({ moduleName: "notes-page" });
This is the entry point for a NativeScript application. It uses the application module and its start
method to specify the module used for the initial page of the app. In this case, we've specified notes-page
, which means that the module is notes-page.js, the markup is notes-page.xml, and the styling for the page is notes-page.css. This is a convention used in NativeScript, that all files for a specific page have to have the same name.
4. Adding the UI Markup
Open the notes-page.xml file and add the following code:
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="pageLoaded"> <Page.actionBar> <ActionBar title="{{ app_title }}"> <ActionBar.actionItems> <ActionItem tap="newNote" ios.position="right" android.position="actionBar"> <ActionItem.actionView> <StackLayout orientation="horizontal"> <Label text="New Item" color="#fff" cssClass="header-item" /> </StackLayout> </ActionItem.actionView> </ActionItem> </ActionBar.actionItems> </ActionBar> </Page.actionBar> <StackLayout> <StackLayout id="form" cssClass="form-container"> <TextView text="{{ item_title }}" hint="Title" /> <Button text="Attach Image" cssClass="link label" tap="openCamera" /> <Image src="{{ attachment_img }}" id="attachment_img" cssClass="image" visibility="{{ attachment_img ? 'visible' : 'collapsed' }}" /> <Button text="Save Note" tap="saveNote" cssClass="primary-button" /> </StackLayout> <ListView items="{{ notes }}" id="list" visibility="{{ showForm ? 'collapsed' : 'visible' }}"> <ListView.itemTemplate> <GridLayout columns="*,*" rows="auto,auto" cssClass="item"> <Label text="{{ title }}" textWrap="true" row="0" col="0" /> <Image src="{{ photo }}" horizontalAlignment="center" verticalAlignment="center" cssClass="image" row="1" col="0" visibility="{{ photo ? 'visible' : 'collapsed' }}" /> <Button text="delete" index="{{ index }}" cssClass="delete-button" tap="deleteNote" row="0" col="1" horizontalAlignment="right" loaded="btnLoaded" /> </GridLayout> </ListView.itemTemplate> </ListView> </StackLayout> </Page>
When creating app pages in NativeScript, you should always start with the <Page>
tag. This is how NativeScript knows you're trying to create a new page. The xmlns
attribute specifies the URL to the schema used for the XML file.
If you visit the schema URL specified, you can see the definition of all the XML tags that you can use within NativeScript. The loaded
attribute specifies the function to be executed once the page is loaded. We'll take a look at this function definition later on in the notes-page.js file.
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="pageLoaded"> ... </Page>
By default, the app header only contains the title of the app. If you wanted to add other UI components, you'd need to redefine it by using <Page.actionBar>
. Then inside you define the things that you want to see in the header. The title is specified by using <ActionBar>
and setting its title
attribute to the page title that you want.
Below we've used the mustache syntax to output the value of app_title
defined in the notes-page.js file. This is how you output values that are bound to the page.
<Page.actionBar> <ActionBar title="{{ app_title }}"> ... </ActionBar> </Page.actionBar>
To define buttons, first use <ActionBar.actionItems>
as the parent, and each <ActionItem>
will be the buttons that you want to define. The tap
attribute specifies a function to be executed when the button is tapped, while os.position
and android.position
are the positions of the button in iOS and Android.
To specify the button text, you could use the <ActionItem>
's text
attribute. However, NativeScript doesn't currently allow changing the text color of the button through CSS. That's why instead we've used <ActionItem.actionView>
to define the content of the button and to set its text color.
<ActionBar.actionItems> <ActionItem tap="newNote" ios.position="right" android.position="actionBar"> <ActionItem.actionView> <StackLayout orientation="horizontal"> <Label text="New Item" color="#fff" cssClass="header-item" /> </StackLayout> </ActionItem.actionView> </ActionItem> </ActionBar.actionItems>
Next is the actual page content. You can arrange the different elements by using one or more of the layout containers. Below we've used two of the available layouts: StackLayout
and GridLayout
.
StackLayout
allows you to stack all the elements inside of it. By default, the orientation of this layout is vertical, so that each UI component is stacked below the last. Think of lego bricks with a downward flow.
On the other hand, GridLayout
allows you to arrange elements in a table structure. If you've ever used Bootstrap or other CSS grid frameworks then this should seem natural to you. The GridLayout
lets you define rows and columns among which each UI component can be placed. We'll take a look at how this is implemented later on. For now, let's move on to the code.
First, let's define the form for creating a new note. Just like in HTML, you can define attributes such as id
and cssClass
(equivalent to HTML's class
attribute). The id
attribute is attached to an element if you want to manipulate it from code. In our case, we want to animate the form later on. cssClass
is used to specify the CSS class that you will use to style the element.
Inside the form is a text field for entering the note title, a button for attaching an image, the selected image, and a button for saving the note. The image element is only visible if the attachment_img
has a truthy value. That will be the case if an image was previously attached.
<StackLayout id="form" cssClass="form-container"> <TextView text="{{ item_title }}" hint="Title" /> <Button text="Attach Image" cssClass="link label" tap="openCamera" /> <Image src="{{ attachment_img }}" id="attachment_img" cssClass="image" visibility="{{ attachment_img ? 'visible' : 'collapsed' }}" /> <Button text="Save Note" tap="saveNote" cssClass="primary-button" /> </StackLayout>
Next is the list that shows the notes that were already added by the user. Lists are created by using the ListView
component. This accepts items
as a required attribute. The value can either be a plain array or an observable array.
If you do not need to perform any form of update (e.g. deleting or updating a field) to each item in the array, a plain JavaScript array will do. Otherwise, use an observable array which allows you to perform updates to the array and have it automatically reflected to the UI. We'll take a look at how an observable array is defined later on.
Also note that a ListView
can have an itemTap
attribute, which allows you to specify the function to be executed when an item in the ListView
is tapped. But in this case we haven't really added it since we don't need to perform any action when an item is tapped.
<ListView items="{{ notes }}" id="list" visibility="{{ showForm ? 'collapsed' : 'visible' }}"> ... </ListView>
The items in the ListView
can be defined using <ListView.itemTemplate>
. Here we're using a <GridLayout>
to create two rows and two columns. The columns
attribute is used to specify how many columns you want in each row.
In this case, *,*
means that there are two columns, each taking up an equal amount of the available space in the current row. So if the whole row has a total width of 300 pixels, each column will be 150 pixels wide. So basically each *
represents one column, and you use a comma to separate each of them.
The rows
attribute works similarly, but controls the amount of space used by a single row. auto
means it will only consume the amount of space needed by the children of each row.
After defining the columns
and rows
in the GridLayout
, you still have to specify which of its children belongs to which row and column. The first row contains the title of the item (1st column) and the delete button (2nd column). The second row contains the image that was attached to the item (1st column). The row and columns are specified by using the row
and col
attribute for each element.
Also notice the use of horizontalAlignment
and verticalAlignment
. You can think of this as the NativeScript equivalent of HTML's text-align
attribute. But instead of text, we're aligning UI components. horizontalAlignment
can have a value of right
, left
, center
, or stretch
, while verticalAlignment
can have a value of top
, bottom
, center
, or stretch
. Most of these are self-explanatory, except for stretch
, which stretches to take up the available horizontal or vertical space.
In this case, horizontalAlignment
and verticalAlignment
are used to center the image both horizontally and vertically inside its column. And horizontalAlignment
is used on the delete button to align it to the right-most part of the second column.
<ListView.itemTemplate> <GridLayout columns="*,*" rows="auto,auto" cssClass="item"> <Label text="{{ title }}" textWrap="true" row="0" col="0" /> <Image src="{{ photo }}" horizontalAlignment="center" verticalAlignment="center" cssClass="image" row="1" col="0" visibility="{{ photo ? 'visible' : 'collapsed' }}" /> <Button text="delete" index="{{ index }}" cssClass="delete-button" tap="deleteNote" row="0" col="1" horizontalAlignment="right" loaded="btnLoaded" /> </GridLayout> </ListView.itemTemplate>
We haven't specified an itemTap
attribute for the ListView
. Instead, we want to attach a delete action that will be executed whenever a delete button inside a list item is tapped. Each item has an index
attribute, which we're passing as a custom attribute for the delete button. This is the unique key used for identifying each item so that we can easily refer to them when needed.
Also notice the loaded
attribute. Just as <Page>
has a loaded
attribute, buttons can also have one. You'll see later on how this is used.
5. JavaScript Code
Now we're ready to look at the JavaScript that makes it all work. In this section, we'll code the notes-page.js file.
Initialization
First we import the data/observable
and data/observable-array
modules. These are built-in modules in NativeScript that allow us to create observable objects and arrays. Observables allow us to automatically update the UI whenever these objects and arrays get updated.
In our app, pageArray
is used for storing the array of notes, and pageData
is used for tying it to the page. pageData
also serves as the general container for all data that we want to show in the UI.
var Observable = require("data/observable"); var ObservableArray = require("data/observable-array"); var pageArray = new ObservableArray.ObservableArray(); var pageData = new Observable.Observable({ notes: pageArray });
Next, import all the other modules that we'll be using in this page:
-
camera
: for working with the device camera. -
view
: for referring to specific elements in the page. Think of it as the equivalent ofdocument.getElementById
in NativeScript. -
ui/enums
: a global dictionary of constant values for anything related to UIs. -
ui/animation
: for animating elements. -
application-settings
: for persisting local data. -
file-system
: for working with the filesystem.
var cameraModule = require("camera"); var view = require("ui/core/view"); var uiEnums = require("ui/enums"); var animation = require("ui/animation"); var appSettings = require("application-settings"); var fs = require("file-system");
Next, initialize the values for the variables that will be used throughout the whole file. page
is used for storing a reference to the current page, notesArr
is the plain array copy of the current notes in the page, and current_index
is the initial value of the index that is used as the unique ID for each note.
var page; var notesArr = []; var current_index = -1;
The pageLoaded()
Function
Functions become available in the context of the page by means of using exports
. Earlier in the notes-page.xml file, you saw that the pageLoaded()
function is executed when the page is loaded.
exports.pageLoaded = function(args) { ... }
Inside the pageLoaded()
function, we'll start by getting the reference to the page. Then we show the form for creating a new note, and get the currently stored values of the new note title and the notes from the application settings.
page = args.object; pageData.set('showForm', true); var new_note_title = appSettings.getString('new_note_title'); var notes = appSettings.getString('notes');
Next, still within the pageLoaded()
function, check if there are notes that are stored locally. If not, we create an array of notes. This array will serve as the default content for new users of the app. However, if there are already some notes stored locally, we convert them to an array and then push that data to the observable array.
Note that before we push the items into the observable array, we first check if it's empty. We have to do this because using the camera module executes the loaded
event on the page once again after an image is selected. This means that if we're not careful, we'll end up pushing duplicates into the array every time the user uses the camera.
if(!notes){ notes = [ { index: 0, title: '100 push ups' }, { index: 1, title: '100 sit ups' }, { index: 2, title: '100 squats' }, { index: 3, title: '10km running' } ]; }else{ notes = JSON.parse(notes); } notesArr = notes; if(!pageArray.length){ for(var x = 0; x < notes.length; x++){ current_index += 1; pageArray.push(notes[x]); } }
Now that we have the notes data set up, we can update the page title by setting its item_title
attribute to the value that we got from the application settings earlier. Then bind pageData
to the page so that the UI automatically gets updated whenever a change is made to the items that we've set.
pageData.set('item_title', new_note_title); args.object.bindingContext = pageData;
Animate the form for creating new notes. We do this by using the getViewById
function in the view
and passing in the context (the current page) as the first argument and the id
attribute assigned to the element that you want to manipulate.
Next, call the animate
function. This accepts an object containing the animation settings. Here we want the form to slide down 160 pixels from its original position over a period of 800 milliseconds.
view.getViewById(page, 'form').animate({ translate: { x: 0, y: 160 }, duration: 800, });
The newNote()
Function
The newNote()
function is executed when the user taps on the New Item action item on the header. This hides and shows the new item ListView
and slides the form up or down depending on the current value of showForm
.
If showForm
is true
, which means it's currently being shown, we change the opacity of the ListView
to 1
over the course of 400 milliseconds, and then slide the form up to hide it. Otherwise, we hide the ListView
and slide the form down.
exports.newNote = function() { var showForm = pageData.get('showForm'); var top_position = (showForm) ? -160 : 160; var list_visibility = (showForm) ? 1 : 0; view.getViewById(page, 'list').animate({ opacity: list_visibility, duration: 400 }); view.getViewById(page, 'form').animate({ translate: { x: 0, y: top_position }, duration: 800, }); pageData.set('showForm', !showForm); }
The btnLoaded()
Function
In the notes-page.xml file, we have a loaded
attribute in the button for deleting a note. This is the function that gets executed when that event happens.
By default, the function assigned to the itemTap
attribute in the ListView
won't get executed when a button is defined inside a ListView
item. This is because NativeScript assumes that the actions to be performed for each list item can be triggered only from those buttons.
The code below is a workaround for that default behavior. This basically removes the focus on the delete button so that you can still execute a function when a user taps on a ListView
item. In this case, we don't really need this code since we haven't assigned any functionality to item taps, but it is a good tool to have when working with lists.
exports.btnLoaded = function (args) { var btn = args.object; btn.android.setFocusable(false); }
The openCamera()
Function
Next is the openCamera()
function, which gets executed when the user taps on the Attach Image button. The current state is not maintained when using the camera module, so we need to save the title of the new note into the application settings first.
Afterwards we can launch the default camera app in the device by calling the takePicture()
method. This method accepts an object containing the picture settings. Once the user has taken a picture and tapped on the Save button in Android or the use image button on iOS, the promise resolves and the callback function passed to then()
gets executed.
The actual image is passed as an argument to the function, and we use this to save the file to the documents path. Once that's done, we save the full file path to the app settings and the current app state so that we can get the value later, before saving the note.
exports.openCamera = function() { appSettings.setString('new_note_title', pageData.get('item_title')); cameraModule.takePicture({width: 300, height: 300, keepAspectRatio: true}).then(function(img) { var filepath = fs.path.join(fs.knownFolders.documents().path, "img_" + (new Date().getTime() / 1000) + ".jpg"); img.saveToFile(filepath, uiEnums.ImageFormat.jpeg); appSettings.setString('new_note_photo', filepath); pageData.set('attachment_img', filepath); }); }
The saveNote()
Function
The saveNote()
function is executed when the user taps on the Save Note button. This gets the current value of the note title text field and the image path, increments the current_index
, and pushes the new item into the plain notes array and observable notes array. Then it saves the current notes and the current_index
into the application settings, removes the values for the new note from the application settings, updates the UI so that the form shows its empty state, and shows the list while hiding the new note form.
exports.saveNote = function() { var new_note_title = pageData.get('item_title'); var new_note_photo = pageData.get('attachment_img'); current_index += 1; var new_index = current_index; var new_item = { index: new_index, title: new_note_title, photo: new_note_photo, show_photo: false }; notesArr.push(new_item); pageArray.push(new_item); appSettings.setString('notes', JSON.stringify(notesArr)); appSettings.setNumber('current_index', new_index); appSettings.remove('new_note_title'); appSettings.remove('new_note_photo'); pageData.set('showForm', false); pageData.set('item_title', ''); pageData.set('attachment_img', null); view.getViewById(page, 'list').animate({ opacity: 1, duration: 400 }); view.getViewById(page, 'form').animate({ translate: { x: 0, y: -160 }, duration: 800, }); }
The deleteNote()
Function
Lastly, we have the deleteNote()
function which gets executed when a user taps on the delete button inside a list item. As you have already seen from previous functions, an object is passed in as an argument to functions that are attached as an event handler for a specific component. This object has the object
property, which refers to the component itself.
From there, you can get the value of an attribute that was passed to it. In this case, we're getting the value of the index
attribute, and we use it to get the actual index of the item that we want to delete.
exports.deleteNote = function(args){ var target = args.object; var index_to_delete = notesArr.map(function(e) { return e.index; }).indexOf(target.index); notesArr.map(function(item, index){ if(index == index_to_delete){ notesArr.splice(index_to_delete, 1); pageArray.splice(index_to_delete, 1); return false; } }); appSettings.setString('notes', JSON.stringify(notesArr)); }
6. Adding Styles
Open the app.css file and add the following global styles:
ActionBar { background-color: #b898ff; color: #fff; } .header-item { text-transform: uppercase; } .item { padding: 20; font-size: 20px; } .form-container { background-color: #fff; margin-top: -160px; padding: 20px; z-index: 10; } .label { font-size: 18px; } .link { text-align: left; background-color: transparent; color: #0275d8; padding: 5px; margin: 10px 0; text-transform: uppercase; font-size: 15px; } .image { width: 300; margin: 20 0; } .primary-button { padding: 5px; color: #fff; background-color: #0723bb; text-transform: uppercase; } .delete-button { font-size: 15px; background-color: #f50029; color: #fff; }
If you want to apply page-specific styles, you can also create a notes-page.css file and define your styles in there.
7. Running and Debugging the App
You can run the app on your device by executing tns run
and then the platform where you want to deploy. Here's an example for android:
tns run android
This automatically installs the Android platform for you if it hasn't already been installed and then runs the app on your device once it's installed. Once the app is running, you can execute tns livesync android --watch
to automatically refresh the app every time you make changes to the source files.
Debugging
Just like any other app framework, NativeScript allows developers to debug their app. This is done with the Chrome dev tools. There are two ways of doing this:
- If have an app already running, you can open a new terminal window and execute
tns debug android --start
to attach a debugger to the currently running instance of the app. - If you don't have an app running yet, use
tns debug android --debug-brk
to build an instance of the app with a debugger attached to it.
No matter which option you choose, this will open up a new tab in the Google Chrome browser that allows you to debug the app just like a normal JavaScript web app. This means that you can actually use console.log
in your source code to inspect the contents of the variables that you're working with.
Conclusion and Next Steps
In this article, you've learned how to build an app with NativeScript. Specifically, you've learned concepts such as using UI components, layouts, styling, animations, using the device camera, and maintaining application state with application settings.
Now that you've built your first NativeScript app, it's now time to take your skills even further by checking out what else you can do with NativeScript and building some more apps with it.
- Build a grocery management app by following the NativeScript Getting Started Guide.
- If you know Angular and you want to use it to build NativeScript apps, you can also follow the tutorial on Building Apps with NativeScript and Angular 2.
- Check out the list of verified plugins and some of the plugins available at npm to have an idea of the different functionality that you can include on your next app without having to touch on the Native API that NativeScript exposes.
- If you want to learn how to lay out your apps better, check out the article on Demystifying NativeScript Layouts by Jen Looper.
Comments