How to Write NativeScript Plugins and Why They’re Easier Than Cordova Plugins

Last week, Max Lynch from Ionic wrote an excellent guide on how to write Cordova plugins, detailing the specific steps you need to take to build and distribute a plugin in the Cordova ecosystem. Here at Progress the article really resonated with us, because through our Telerik background we have a lot of experience in the Cordova plugin world. We maintain a large collection of verified plugins at plugins.telerik.com; we sell premium tooling for building and testing Cordova apps; and we regularly speak about our plugin experiences at places like PhoneGap Day.

Because of this background we know that Cordova plugins are powerful, but we also know that these same Cordova plugins can be difficult to create. You can get a sense of the complexity by quickly skimming through Max’s article; you’ll see a lot of words like “obscure” and “esoteric”, as well as a decent amount of ceremony to build a plugin that logs a string and outputs a date.

The difficultly of accessing native APIs in Cordova is one of the reasons we built NativeScript, and why designed NativeScript to give you direct access to iOS and Android APIs in your JavaScript or TypeScript code. This direct access negates the entire need to write a plugin to access features of the underlying native platforms. For instance, you can recreate the entirety of Max’s Android getDate() call with one line of code in your NativeScript app.

(new java.util.Date()).toString();

Pretty cool, huh?

If you’re curious how this JavaScript code is able to allocate a Java Date object and invoke its toString() method, feel free to read more about how NativeScript works.

But even though you can access these APIs directly, we also realize there’s a lot of power in abstracting these native API calls behind far easier-to-use JavaScript APIs. I think most developers would prefer to write getDate() rather than deal with classes like java.util.Date or NSDateFormatter and NSLocale. That’s why we spent a lot of optimizing our NativeScript plugin model. Our goal was to give developers the ability to quickly incorporate native code without writing native code, and to also greatly reduce the barrier to entry for plugin authors.

To show how this works in action, I’m going to go through the same steps Max did in his article, but I’ll be building a NativeScript plugin instead of a Cordova one. Max creates a plugin with an echo() and getDate() method that runs on iOS and Android, and therefore I will do the same in this article. By comparing NativeScript and Cordova plugin authoring side by side, hopefully you’ll see just how much easier NativeScript makes accessing native code.

It’s worth noting that just because we believe NativeScript has an easier-to-use plugin model, that doesn’t make NativeScript a “better” technology. The right technology choice depends on what you’re building, and it always makes sense to evaluate your options before committing to a framework for a long-term endeavor. We at Progress love Cordova, and continue to build a lot of tooling based on the Cordova architecture. If you’re looking for more thorough information on choosing between NativeScript & Cordova, check out a chat we had with our Telerik Developer Experts on the topic last month.

1. Getting started: scaffolding our first plugin

NativeScript plugins were designed to be simple. Although there are more fully-featured community-written plugin generators to scaffold advanced plugins, the simplest NativeScript plugins are just three files. Here’s what the world’s simplest NativeScript plugin looks like. You can create these as blank files for now, and we’ll look at what goes into these files as we complete this guide.

└── MyNativeScriptPlugin
    ├── index.android.js
    ├── index.ios.js
    └── package.json

2. Configuring our Plugin

Cordova uses a plugin.xml file for configuration, and in NativeScript we use the package.json file you can see in the file structure above. NativeScript plugins are distributed through npm, so if you’ve used npm before in another JavaScript project, you already know most of what you need to know to configure a NativeScript plugin. And if not, you can refer to npm’s extensive documentation on the topic, and reuse that knowledge when developing libraries for other JavaScript-based apps.

To give you an idea of what a NativeScript plugin’s package.json looks like, here’s the full configuration for the NativeScript flashlight plugin.

{
  "name": "nativescript-flashlight",
  "version": "1.0.1",
  "description": "A flashlight NativeScript module for Android and iOS",
  "main": "index.js",
  "nativescript": {
    "platforms": {
      "ios": "1.1.0",
      "android": "1.1.0"
    }
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/tjvantoll/nativescript-flashlight.git"
  },
  "keywords": [
    "NativeScript"
  ],
  "author": "TJ VanToll <tj.vantoll@gmail.com> (http://tjvantoll.com/)",
  "license": {
    "type": "MIT"
  },
  "bugs": {
    "url": "https://github.com/tjvantoll/nativescript-flashlight/issues"
  }
}

The only thing in this package.json that’s unique to NativeScript is the "nativescript" object that starts on line 5. This object tells the NativeScript CLI the minimum NativeScript runtime version required to run this plugin — in this case, version 1.1.0 of both the iOS and Android runtimes. For the most part you can just set this object to the latest NativeScript version and forget about it, but if a change in NativeScript breaks your plugin, this object gives you the ability to specify exactly what versions of NativeScript your plugin supports.

Personally, I start new NativeScript plugins by copying the flashlight plugin’s package.json contents verbatim, changing the small handful of names and descriptions, and calling my configuration complete.

3. Building our plugin: JavaScript

In NativeScript, writing your JavaScript plugin involves exposing a JavaScript API from the index.android.js and index.ios.js files we created as empty files in step 1. Unlike Cordova, you don’t have to code to any framework-specific APIs to write a plugin; there’s no equivalent of Cordova’s exec() function, for instance.

In fact, you don’t even have to provide separate index.js files for Android and iOS. The .android.js and .ios.js naming convention is a convenience NativeScript provides to separate your Android and iOS code, but you could choose to put all of your code in a single index.js file if you choose.

For this example we’ll use separate files for iOS and Android, and therefore we’ll put the following code in both our index.android.js and index.ios.js files to define the plugin API.

module.exports = {
  echo: function() {}
  getDate: function() {}
}

In NativeScript we use the same CommonJS specification that Node.js uses, so this file is going to look identical to a JavaScript module you’d write for that environment. It’s worth noting that we also support TypeScript as a first class citizen, and if you were to write this plugin in TypeScript it’d look something like this:

export class MyNativeScriptPlugin {
  static echo() {}
  static getDate() {}
}

Regardless of the approach you use, you can again see that nothing you’re doing here is proprietary to NativeScript. The code and techniques you’re writing is totally reusable in Node, as well as other JavaScript-based environments.

4. Building our plugin: Native iOS

Much like Cordova, writing native code is the hardest part of building a NativeScript plugin. The good news is in NativeScript you don’t have to write a single line of Objective-C, Swift, or Java. All of the platform APIs are available to you in JavaScript directly, so you can just use them, and it’ll save you a whole lot of code. For example, here’s the full iOS implementation of Max’s Cordova plugin, which includes an interface file and an implementation file (per Objective-C conventions).

MyCordovaPlugin.h

#import <Cordova/CDVPlugin.h>

@interface MyCordovaPlugin : CDVPlugin {
}

// The hooks for our plugin commands
- (void)echo:(CDVInvokedUrlCommand *)command;
- (void)getDate:(CDVInvokedUrlCommand *)command;

@end

MyCordovaPlugin.m

#import "MyCordovaPlugin.h"

#import <Cordova/CDVAvailability.h>

@implementation MyCordovaPlugin

- (void)pluginInitialize {
}

- (void)echo:(CDVInvokedUrlCommand *)command {
  NSString* phrase = [command.arguments objectAtIndex:0];
  NSLog(@"%@", phrase);
}

- (void)getDate:(CDVInvokedUrlCommand *)command {
  NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  NSLocale *enUSPOSIXLocale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
  [dateFormatter setLocale:enUSPOSIXLocale];
  [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];

  NSDate *now = [NSDate date];
  NSString *iso8601String = [dateFormatter stringFromDate:now];

  CDVPluginResult *result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:iso8601String];
  [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
}

And here’s what that same implementation looks like in NativeScript:

module.exports = {
  echo: function(phrase) {
    console.log(phrase);
  },
  getDate: function() {
    var dateFormatter = NSDateFormatter.alloc().init();
    var locale = NSLocale.localeWithLocaleIdentifier("en_US_POSIX");
    var now = NSDate.date();

    dateFormatter.locale = locale;
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ";

    return dateFormatter.stringFromDate(now);
  }
}

Note: Why console.log instead of NSLlog? Because NSLog is a variadic function, you would normally have to take one extra step to use that API in NativeScript (see the documentation). However, NativeScript’s global console object is implemented with the same NSLog API, so you get the same result using it.

Personally, I think iOS APIs like NSDateFormatter and NSLocale look arcane regardless of how I write them, but in NativeScript I appreciate two things:

  1. I get to drop a whole lot of Cordova and Objective-C boilerplate code.
  2. I get to stay in a language where I understand the syntax and semantics.

There is a bit of a learning curve for figuring out how to transfer Objective-C and Swift code into NativeScript JavaScript code, but in most cases the transfer is relatively straightforward.

The biggest thing you have to learn is the unique, and quite frankly bizarre, syntax Objective-C uses for method calls.

[NSDate date]

Yep, that’s a method call. Who knew? In NativeScript since you’re writing JavaScript, you have to switch this code to use the JavaScript syntax for method invocation, or NSDate.date().

The other big thing to know is how to transfer “set” method calls from Objective-C to NativeScript. For example consider the code below.

[dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"];

This code calls the dateFormatter object’s setDateFormat() method, and passes that method an argument of "yyyy-MM-dd'T'HH:mm:ssZZZZZ". In NativeScript this setter becomes a more straightforward property assignment using JavaScript semantics.

dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ";

These conventions can take some time to get used to, but once you do, I think you’ll find writing in NativeScript a lot easier than writing in Objective-C or Java.

And there are some massive advantages to writing all your code in one language. To start, you can write your code in the editor you’re already comfortable with, so there’s no need to hop between Xcode, Android Studio, and your editor of choice. You can also reuse JavaScript or TypeScript linters and style checkers.

One other advantage is that if you choose to write your plugin in TypeScript, which we recommend for non-trivial plugins, we provide complete TypeScript declaration files for both iOS and Android. So you can get Xcode-like features like syntax checking and code complete, without actually having to use Xcode. The same is true with Android and Android Studio.

5. Building our Android plugin

Again much like Cordova, building the Android portion of a NativeScript plugin is conceptually similar to building the iOS portion. In NativeScript, building the Android version of a plugin is a bit easier because Java as a language is syntactically closer to JavaScript than Objective-C. For example, here’s the full source of Max’s Android implementation.

package com.example;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.apache.cordova.PluginResult.Status;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;

import android.util.Log;

import java.util.Date;

public class MyCordovaPlugin extends CordovaPlugin {
  private static final String TAG = "MyCordovaPlugin";

  public void initialize(CordovaInterface cordova, CordovaWebView webView) {
    super.initialize(cordova, webView);

    Log.d(TAG, "Initializing MyCordovaPlugin");
  }

  public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
    if(action.equals("echo")) {
      String phrase = args.getString(0);
      // Echo back the first argument
      Log.d(TAG, phrase);
    } else if(action.equals("getDate")) {
      // An example of returning data back to the web layer
      final PluginResult result = new PluginResult(PluginResult.Status.OK, (new Date()).toString());
      callbackContext.sendPluginResult(result);
    }
    return true;
  }

}

And here’s what the same functionality looks like in NativeScript:

module.exports = {
  echo: function() {
    android.util.Log.d("MyNativeScriptPlugin", "Initializing MyNativeScriptPlugin");
  },
  getDate: function() {
    return (new java.util.Date()).toString();
  }
}

As you can see, the difference in the amount of code you have to write is drastic. With NativeScript there are no Cordova APIs to write to, and no Java boilerplate to worry about; you just write the APIs you need. The one tip to keep in mind is that in NativeScript you do need to fully qualify any Java APIs that you intend to use. In this case new java.util.Date() invokes a new Java Date object, and toString() invokes that object’s toString() method.

6. Testing plugins

Cordova has a cordova plugin add command, and in NativeScript we have a tns plugin add command. If you’re testing a plugin locally you can point the command directly at the source:

tns plugin add /path/to/MyNativeScriptPlugin

And once you publish your plugin to npm, you can call the exact same command, but pass it the npm module name instead of a path. For instance, the following command installs the NativeScript flashlight plugin.

tns plugin add nativescript-flashlight

That’s really all there is to it. The great thing about working in NativeScript is, unlike Cordova, you can start by just writing your plugin as a function inside one of your existing apps. If you think that function would be useful in other projects, you can abstract that function into its own file, and perhaps even publish it to npm so everyone can benefit from the abstraction you created.

7. Where to go from here

There’s a whole lot more you can do with NativeScript plugins. We haven’t even touched on the fact that you can easily import native libraries such as CocoaPods for iOS and package them directly within your plugin. Or that you can build plugins for user interface components.

If you’re interested in building a NativeScript plugin, your first step is to learn a bit about NativeScript itself first. Start by going through one of our getting started guides to learn the basics of what NativeScript is and how to build apps with the framework.

When you’re ready to write a plugin, start by looking at a few existing plugins that exist on npm. It’s a good idea to start with a simple plugin such as flashlight while you’re getting up and running. As you get more advanced, you may want to use one of our community-written plugin generators, which can generate plugins with fully-functional demo projects, TypeScript support, and more.

If you need help during the plugin process, feel free to ask any questions you have on Stack Overflow with the “NativeScript” tag, or reach out to us in the NativeScript Community Slack Team. We even have a #plugins channel dedicated to plugin authoring.

Best of luck, and happy NativeScript-ing!

Header image courtesy of Samuel M. Livingston

Comments

  • yesimahuman

    I’m not surprised to see this this coming from you guys, unfortunately. Your marketing approach has always been to put down other projects in order to prop up your own. I refuse to be drawn into that, I’d rather make awesome stuff.

    NS is a nice product, no arguing there. The JS/native wrapper is an interesting approach and Cordova is hardly restricted from being able to use it (http://microsoft.github.io/ace/ ). In fact, I don’t see why Cordova projects couldn’t use NativeScript for your bridge while keeping a lot of the content web based (which is a philosophical difference). Wouldn’t that be great for everyone?

    I wish you would put your effort into improving the awesome Cordova project and ecosystem (which you’ve done with the verified plugins and sponsoring Eddy, kudos to you) instead of talking trash about it and using contributors word’s against them. This was done in poor taste and I’ve lost respect for you guys.

    • remotesynth

      Obviously, I have my own biases, but I’d argue that your comment is way off base. Notably, you don’t cite a single instance where this comparison is unfair or where it “talks trash”. Rather, you take issue with the entire concept of competition as unfair. It is not as though NativeScript is some sort of Cordova or Ionic clone – its approach is entirely different (and decidedly different from Project ACE as well). And if we didn’t think our solution offered some significant advantages (of which, direct access to native APIs is one), then we shouldn’t be putting the effort.

    • 😮

    • Nathanael A

      Seems a bit weird your response, unless you are seriously invested in the ionic community and are worried about it for some reason — otherwise your response seems crazily out of proportion to the article — maybe a bad day colored your reading of the article?

      I read through the article and didn’t see anything “putting” anything down or talking trash. In fact he did the opposite and even complimented Max for his “excellent” plugin article as the very first line, and he had a callout stating that NS might not be the best tool and that you need to evaluate the best tool for the job! That was being a hell of a lot more generous than I would have been.)

      Nothing negative that I could see other than the direct comparison that writing a plugin in Cordova is considerably more challenging than writing a plugin in NativeScript. Truth isn’t trash talking.

      This is the type of article I as a developer are very interested in because it shows in a fairly clear manner the differences in building a plugin in both NS and Cordova. You had Max who is extremely knowledgeable about all things Cordova, create an excellent article on how to create a plugin in Cordova, I’m sure he did it in the most sane method possible as it was to help promote making plugins in Cordova. (And for that he deserves Kudos).

      Then you had T.J. do the exact same thing, using NS as the base. That is what makes it an awesome compare and contrast and I love meaty articles that show me real code and how to do real things. If anything I would love to see other articles doing compare and contrast between cordova/ionic vs NativeScript vs Fuse vs React Native vs Titanium. It is like that “TODO” app that all the web frameworks do; these types of articles gives depth on how you have to do certain things in the mobile systems… If anything Max, T.J., the DevReps at Fuse, Xamarin, and React Native should all get together and maybe do an article series on the strengths of there respective platforms; and show how to do basic things in simple code. That way the developers evaluating the platforms could easily read the articles and see which coding style makes the most sense to them and go full tilt with that platform…

      Disclaimer; I am invested in the NS community; and have written a huge number of plugins, articles, etc; but still I didn’t see anything in this article that trash talked cordova, I just saw it point out the difficulty in making a plugin in cordova vs NS.

  • Pingback: Dew Drop - August 19, 2016 (#2312) - Morning Dew()

  • Nice starting article. I really want to use NativeScript for my product, but the problem I’m facing is that I have a 3rd PArty native UI component that I need to integrate (Android and iOS), that’s not just the standard native components. I’ve been struggling with https://docs.nativescript.org/plugins/ui-plugin and googling around and follow the try-fail approach without success. Do you have a working example of a 3rd party native UI component integrated with NativeScript? Thanks. Sebastian

  • Daviz

    Thanks for this great article.