Removing platform specific code with webpack in your NativeScript application

The NativeScript framework automatically includes source files that are suffixed with the given platform name. This way the clutter and size of your built application is decreased and you do not distribute dead platform specific code that isn’t needed. But often there are case where is it much more simple to write your platform specific code in a single file and just surround it with if statements for the platform, rather then separating it into separate platform specific files. So what happens then? Let’s take an example – bellow you will see the natvigatingTo event handler for a page which has some platform specific code:

import { EventData } from "data/observable";
import { isIOS, isAndroid } from "platform";
import * as frame from "ui/frame";
import { Page } from "ui/page";
import { HelloWorldModel } from "./main-view-model";

export function navigatingTo(args: EventData) {
    let page = <Page>args.object;

    if (isIOS) {
        console.log("test");
        frame.topmost().ios.controller.view.window.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(242 / 255, 242 / 255, 242 / 255, 1);
        UITableViewCell.appearance().selectionStyle = UITableViewCellSelectionStyle.None;
        UITableView.appearance().tableFooterView = UIView.alloc().initWithFrame(CGRectZero); // Hide empty cells
    }

    console.log(`We are running on ${isAndroid ? "ANDROID" : "IOS"}`);

    page.bindingContext = new HelloWorldModel();
}

If we run that though webpack with uglify (and you should always do that for the version of an app that you publish to the stores) we will get this (note that I reformatted code with new lines for readability):

function i(e) {
    var t = e.object;
    o.isIOS && (
        console.log("test"),
        a.topmost().ios.controller.view.window.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(242 / 255, 242 / 255, 242 / 255, 1),
        UITableViewCell.appearance().selectionStyle = 0,
        UITableView.appearance().tableFooterView = UIView.alloc().initWithFrame(CGRectZero)
    ),
        console.log("We are running on " + (o.isAndroid ? "ANDROID" : "IOS")),
        t.bindingContext = new r.HelloWorldModel
}

As you see, although changed a bit, all the platform specific code is there. And no matter if you bundle for iOS or Android the code will be the same. Since you cannot change the platform during runtime some parts of that code are dead and will never execute. So how can we remove those? This can easily be achieved by helping webpack “understand” which parts of the code are dead so it can remove them when it optimizes the bundle.

First thing that we will do is instead of using the isAndroid / isIOS flags from the platform module, we will use flags that we will define in the global object (you will see why later on). In the app’s entry point before starting it we will extend the global object with the flags and their values:

import { isIOS, isAndroid } from "platform";
Object.assign(global, { isIOS, isAndroid });

Now in order to make TypeScript happy and so that we can directly use ‘global.isIOS’ and ‘global.isAndroid’ lets add an ‘app.d.ts’ in our app with the following content:

// Augment the NodeJS global type with our own extensions
declare namespace NodeJS {
    interface Global {
        // Add custom properties here.
        isIOS: boolean;
        isAndroid: boolean;
    }
}

This extends the definition of the global object that is provided with NativeScript’s core modules and we won’t have to cast global to any in order to make our code compile.

Now if we refactor our platform specific code to use the global object instead, the code will look like this:

export function navigatingTo(args: EventData) {
    let page = <Page>args.object;

    if (global.isIOS) {
        console.log("test");
        frame.topmost().ios.controller.view.window.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(242 / 255, 242 / 255, 242 / 255, 1);
        UITableViewCell.appearance().selectionStyle = UITableViewCellSelectionStyle.None;
        UITableView.appearance().tableFooterView = UIView.alloc().initWithFrame(CGRectZero); // Hide empty cells
    }

    console.log(`We are running on ${global.isAndroid ? "ANDROID" : "IOS"}`);

    page.bindingContext = new HelloWorldModel();
}

If we run webpack with uglify now we wont see much of a difference. This is because webpack still does not know which code is dead. So lets help him understand that by changing the webpack.config.js and adding to the webpack.DefinePlugin the same global.isIOS and global.isAndroid flags. This is possible because during bundle time webpack already knows for which platform it is creating the bundle:

// Define useful constants like TNS_WEBPACK
new webpack.DefinePlugin({
    "global.TNS_WEBPACK": "true",
    "global.isAndroid": `${platform === "android"}`,
    "global.isIOS": `${platform === "ios"}`,
    "process": undefined,
}),

When the bundle is created webpack will replace the occurrence of those two flags with the respective value as defined in the config file. By doing this it will render parts of the code dead and the UglifyJS plugin can safely remove them from the final bundle.

One last thing to do – since we will not actually need the flags in the global object when the code is bundled, we can safely change the code in the app entry point:

if (!global.TNS_WEBPACK) {
    Object.assign(global, { isIOS, isAndroid });
}

This will ensure that both bundled and non-bundled builds are working as expected and we will not be polluting the global object unnecessary.

Now if we bundle and uglify the code, for iOS we will get (again code reformatted for readability):

function r(e) {
    var t = e.object;
    console.log("test"),
        o.topmost().ios.controller.view.window.backgroundColor = UIColor.colorWithRedGreenBlueAlpha(242 / 255, 242 / 255, 242 / 255, 1),
        UITableViewCell.appearance().selectionStyle = 0,
        UITableView.appearance().tableFooterView = UIView.alloc().initWithFrame(CGRectZero),
        console.log("We are running on IOS"),
        t.bindingContext = new a.HelloWorldModel
}

And for Android we will get:

function a(t) {
    var e = t.object;
    0;
    console.log("We are running on ANDROID");
    e.bindingContext = new o.HelloWorldModel
}

As you will notice the code for both platforms is greatly optimized now. First the conditional expression we had in the last console.log is changed so there is no unnecessary comparison. Furthermore for Android all the iOS specific code has been removed and replaced with a dummy 0.

Implementing In-App purchases in your NativeScript application (Part 3)

After I showed you how to create a simple NativeScript application that implements in-app workflow using the nativescript-purchase plugin and how to set up and sandbox test the workflow on iOS. Now it is time to do same thing on Android.

Preliminary Notes

Before we start you must understand that if you want to test the full workflow you will either need a physical Android device, or your emulator must have the full-blown Google Play services installed (with the Play Store and other stuff). So you cannot test this on the stock Android emulator that comes with Android SDK. This is because the in-app purchase services extensively use the play store to communicate with Google’s services.

Also I’m assuming that your Google account (the one with which you publish your app) is set up as a merchant account, which is needed if you want to use in-app purchases. If it is not you can take a look at this help article on how to set it up.

Set up test accounts

The first step we will take is to add which Google accounts will be able to sandbox test the purchases in our app. In order to do so you must go to the Google Play Console->Settings->Developer account->Account details. Scroll down to the License Testing section and in the field add as a comma-separated value the Google Accounts that will be treated as sandbox test users.

NB: Those accounts are considered sandbox purchase testers for ALL applications that are published through your Google Play developer account.

Create and set up the application in Google Play console

Next we should add our application to the Google Play developer console. I will not go in details on how you create it as there is nothing special compared to creating a normal application. Just make sure you fill in all the needed details (like screenshots, feature graphic, icons, etc.) as unlike iOS for Android we will actually need to publish our app. You will also need to upload your binary. Since my demo app does not do anything, I will be uploading it to the Alpha release with a closed Alpha testing so it is not public on the store.
Once you have created the application, before we publish it we need to add our in-app products. So lets head over to the In-app products section in the Google Play console and add our first product

Remember that for now the nativescript-purchase plugin does not support subscriptions so make sure you select the Managed Product option and also make sure you type the exact same product id we have set up in the initialization of the purchase plugin: org.nativescript.purchasesample.product1. Next is to set up the product details and pricing (I’m using the same description as for iOS):

Once you save this product remember to make it active via the drop down next to the save! 🙂
We repeat the above procedure for the second product org.nativescript.purchasesample.product2. Although this is our consumable product for Android there is no set up difference between the two like it was for iOS. The consuming is done in our NativeScript code. After you add the second product, your in-app product listing should look similar to this:

Now we are ready to publish our application! Note that it can take up to several hours for the application and in-app products to be published to Google Play, so we must wait for this to happen, before we can test it.

Sandbox testing purchases

Once the application is in Published status we can start our testing. Note that if you used a closed Alpha testing option you need to go to the Releases menu and open the Opt-in URL with the accounts which you assigned as testers.

NB: You cannot sandbox test purchases by deploying your app via tns run android or by any other means. You NEED to install the application from the Google Play store.

Now lets head over to the Play Store and find our app. Make sure you are signed in on the device with the same tester account you have added as a license test user at the start. In my experience it is sometimes very difficult to find the app in the store via search as it takes some additional time after publishing for Google to correctly update their index. So it might be best to just open the play store listing for the app directly by opening the following Url in the device’s browser: https://play.google.com/store/apps/details?id=<PACKAGE ID>

So lets install and open it. If everything is done correctly you should see a similar to the iOS listing of the products we have added:

If you tap on a product you will be presented with the popup to confirm your purchase. Take note of the bold line below the price that indicates that this is a sandbox purchase and that your credit card will not be charged.

Once you confirm the purchase you will get a notification that the it was successfully!

NB: You cannot use the same developer Google account that published the app for testing. You must set up and use another Google account for sandbox purchases.

Wrapping it up

As you can see testing on Android is a little bit harder than on iOS, especially if you want to debug something. This is due to the fact that you need to upload your APK to the Play Store, wait for the update to be published and then download it to the device. This makes it really difficult to iron out any bugs or trace problems. That’s why my personal recommendation is to first implement and thoroughly test your workflow and logic on iOS. Once you are sure it is working flawlessly then move to test it on Android.

Implementing In-App purchases in your NativeScript application (Part 2)

In the previous part I’ve shown you how to create a small NativeScript application and add a simple purchase workflow using the nativescript-purchase plugin. Now it is time to wire things for iOS. In this part I will guide through adding the necessary settings on the Apple Developer portal and iTunes Connect, create some sandbox users and test your workflow.

Creating an App ID

The first thing we need to do is register a new App ID in the Apple Developer portal. We do this in the “Certificates, IDs & Profiles” menu, then “App IDs” and click on the “plus” button on the top right. For the App ID, you can select whatever description you want. For the App ID Suffix you must select Explicit App ID and type exactly the name as it is present in the package.json file nativescript section the id attribute. In my case this is org.nativescript.purchasesample. And we need to use Explicit App ID, because wildcard App IDs do not support in-app purcahses.
App ID Setup

Configuring iTunes Connect

Once we have the App ID set up we need to head over to iTunes Connect and add our application there. Note that even if you intend to do Sandbox testing only, you still need to register your app in iTunes Connect. You do not need to upload the app binary or wait for Apple’s approval, it just needs to be registered.
On the New App screen we select iOS and fill in the rest of the details. For Bundle ID make sure you select the bundle that we have just created.
iTunes Connect App Add

NB: It can take up to couple of minutes for iTunes Connect to pick up your new App ID. So if you do not see it in the drop down, don’t worry, just wait 5-10 minutes and it should appear.

Once we have our App created we need to go the App settings screen Features -> In-App Purchases. This is where we will add our products.
iTunes Connect In-App Purchases

Since we are going to do Sandbox testing, you can safely ignore the Information about production products. When we press the plus button to add our first product we are presented with the following wizard that asks us what type of product we will add.
iTunes Connect Add Product

As of writing of this post the nativescript-purchase plugin supports only in-app purchases. So we will be working with the first 2 options. We will create one consumable and one non-consumable products.
We will start by adding a consumable product first. On the New In-App Purchase screen we go and enter the details for our product. When we created the application in the previous part of the tutorial we have said that our consumable product will be product2. So let’s fill in the details for that product:
iTunes Connect In-App Purchase Setup
Reference Name – You can use anything for this. This is not shown to the end user and is only for internal use.
Product Id – This one is very important. You must use the same ID that we used in the init() method of the nativescript-purchase plugin. In our case (and in case you followed the naming from the first part) we should input here org.nativescript.purchasesample.product2.
Pricing – You should select here the pricing tier for this product. You can chose from various ones and select the one that best suits your needs.
Localization (Display Name and Description) – This is what the user will see when you list your products. You can add localized text for both by pressing the plus sign next to the Localization header.

Once we save Product 2 info, we have to go through the above procedure and add Product 1 as well. Note that Product 1 will be a non-consumable product, so make sure you select the second option in the add product popup.

Now let’s return to our NativeScript project and run the app onto the simulator. If we did everything correctly we should see a nice list of products that we just added to iTunes Connect:
Purchase List Simulator

NB: You might NOT be able to see the registered products (consumable, non-consumable, etc.) if you do not have any contracts in effect regarding paid applications. To solve the problem, you can activate the contract for paid applications in App Store Connect > Contracts, Tax and Banking section.

Testing with Sandbox purchases

Now that we have the list ready it is time to do some sandbox purchases and test our purchase workflow. Before we start you must note the following: you cannot make sandbox (or real) purchases in the simulator! So in order to test we must deploy to a real device and in order to do this we first need to set up a provisioning profile. So lets get back to the Apple Developer portal and create a Development provisioning profile for the App ID we created at the start:
Provision Profile Creation Screen

Once you generated the provision profile and download it locally it is time to assign it to our app. In order to do so first do a tns build ios in the terminal in order NativeScript to generate fully the platform specific files. Then in Finder navigate to the platforms/ios folder located under you project’s root folder and open the .xcodeproj file. This should bring up XCode. There click on your project in the left pane and then uncheck the “Automatically manage signing” box and then in the “Signing (Debug)” section import your downloaded provision profile.
XCode Provisioning Setup

Do not close XCode yet as we still need to add the in-app purchase entitlement to the project. Go to the Capabilities section and turn on the In-App Purchase capability. Wait till all actions are complete and then you can close XCode.
XCode In-App Purchase Entitlement

Now we are ready to deploy the app to our real device. Attach your device and then use tns deploy ios to deploy to it. Once the app is deployed, open it and if everything is done correctly you should see the same list of products as in the simulator.
NB: You need to repeat the two XCode steps above every time you do tns platform clean ios on when you upgrade the NativeScript runtime to a new version. This is because those actions recreate the whole platforms/ios folder and its files.

The only thing left is to set up some sandbox users to test our purchases. Apple has a really nice step-by-step guide on how to add your sandbox users to your iTunes Connect account, so I will not go in much details here. The most important thing to remember is that you cannot use your actual Apple ID as a sandbox user. You must set up a new one.
Once you have added your sandbox users go to the App Store app on your device, scroll down to the bottom of the Featured page and tap on your Apple ID and select Sign Out. Do not try to sign in with the sandbox user to the App Store app as you will get an error. Instead go to the sample app we are developing and tap on a product. You will get a prompt asking you to sign in:
Device In-App Purcahse Prompt
Tap on the Use Existing Apple ID option and enter your sandbox user credentials:
Device Sandbox User Sign In
You will get a prompt to confirm your sandbox purchase:
Device Sandbox In-App Purchase Confirm
Once you press Buy and wait for the purchase to complete you will get a prompt of the successful purchase:
Device Sandbox In-App Purchase Success
Congratulations you have just made your first sandbox purchase!

If you want to explore all the properties you can get from the nativescript-purchase plugin for the Product and Transaction you can add some console.logs to the purchase workflow methods we have added in the previous part of the tutorial. Attach your device to your Mac and then use tns run ios so you can see the logs in your terminal.

Coming up next

In the final part of this three part tutorial I will guide you through configuring things for Android on Google Play and making some sandbox purchases on Android.

Implementing In-App purchases in your NativeScript application (Part 1)

Many mobile applications nowadays leverage the in-app purchase capabilities of both Android and iOS which gives users ability to buy digital goods. But since there are some tricks to get the native part going I’ve decided to write a 3-part in depth tutorial that will guide you through the full process of setting up in-app purchases in your NativeScript app. So let’s get going!

Creating the sample application

As of the writing of this article the most recent NatvieScript version is 2.5.1. Make sure you have installed and configured it correctly. For this tutorial we will be using vanilla TypeScript app so let’s run the following and create it:

tns create purchase-sample --tsc

Adding the nativescript-purchase plugin

After we created our sample app it is time to add the nativescript-purchase plugin. The plugin will do the heavy lifting of consuming in-app purchases in our application:

tns plugin add nativescript-purchase

Configuring available purchases for the application

Now it is time to start wiring things in our application. First stop is to specify what purchases will be available to our users. This is done in the app.ts file of the application (the main entry point of the NativeScript app). You can do this by adding the following block just above the app.start(...) line:

import * as purchase from "nativescript-purchase";

purchase.init([
    "org.nativescript.purchasesample.product1",
    "org.nativescript.purchasesample.product2"
]);

With this we are telling that products with ids org.nativescript.purchasesample.product1 and org.nativescript.purchasesample.product2 should be available to the users. Note that it is not necessary to prefix your products’ ids with the application id but this makes it really clear what product with which application is associated. If you do not like those long ids you can use just product1 and product2. Just note what ids you use here as we will need them in the next parts of the tutorial when we set up everything in iTunes Connect and Google Play Store.

Designing the UI

Now let’s move on to creating a simple UI that will show a list of products and give users ability to buy new products or restore their previous purchases (in case they changed their device):

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="navigatingTo" class="page">
    <Page.actionBar>
        <ActionBar title="In-App Purchase Sample" class="action-bar">
            <ActionBar.items>
                <ActionItem ios.position="right" text="Restore" tap="onRestoreTap"/>
            </ActionBar.items>
        </ActionBar>
    </Page.actionBar>

    <GridLayout>
        <ListView items="{{ items }}" itemTap="onProductTap">
            <ListView.itemTemplate>
                <GridLayout rows="auto, auto" columns="*, auto" padding="5">
                    <Label row="0" col="0" class="h2" text="{{ localizedTitle }}"/>
                    <Label row="1" col="0" text="{{ localizedDescription }}" textWrap="true" color="#999999"/>
                    <Label text="{{ priceFormatted }}" row="0" rowSpan="2" col="1" class="h1" style="margin-left: 5"/>
                </GridLayout>
            </ListView.itemTemplate>
        </ListView>
        <ActivityIndicator busy="{{ loading }}" />
    </GridLayout>
</Page>

The UI is quite simple: we have an action bar with a button to restore previous purchases. Then bellow we have a list view that will display the available purchases and when tapped we will trigger our purchase workflow.Finally we have an activity indicator to show the user when we are doing some backend operations.

Purchase workflow code

With the setup and the UI ready it is time to go in the main-page.ts file and write the actual purchase workflow code that we will use to show the available products to the user and allow the user to buy and restore their purchases.
We will start by adding some required imports at the top for things that we will need:

import * as applicationSettings from "application-settings";
import * as purchase from "nativescript-purchase";
import { Transaction, TransactionState } from "nativescript-purchase/transaction";
import { Product } from "nativescript-purchase/product";
import { ItemEventData } from "ui/list-view";

let viewModel: Observable;

The next step will be to add the code that will load our products and put them in the model for the page.:

export function navigatingTo(args: EventData) {
    let page = <Page>args.object;

    viewModel = new Observable({
        items: [],
        loading: true
    });

    page.bindingContext = viewModel;

    purchase.getProducts()
        .then((res) => {
            viewModel.set("items", res);
            viewModel.set("loading", false);
        })
        .catch((e) => console.log(e));
}

NB: For simplicity we are be using a local model for the page that is defined in the same TS file. In a real life scenario you should always separate your models from the code behind of the view!

We set some default values in the model and bind it to the page. Next we call the getProducts() method of the nativescript-purchase plugin that will load our product details from iTunes Connect/Google Play Store – like price, title, etc. You can check all the available product properties here. The method will return an array with the products, which we set in the model and stop the loading animation.

There is one more thing that we should do in the navigatingTo event handler – to subscribe to the exposed transactionUpdated event of the nativescript-purchase plugin where we will handle the transactions made by our users. You can add this at the end of the navigatingTo event handler:

purchase.on(purchase.transactionUpdatedEvent, (transaction: Transaction) => {
    if (transaction.transactionState === TransactionState.Restored) {
        applicationSettings.setBoolean(transaction.productIdentifier, true); /* 1 */
    }
    if (transaction.transactionState === TransactionState.Purchased) {
        if (transaction.productIdentifier.indexOf(".product2") >= 0) { /* 2 */
            purchase.consumePurchase(transaction.transactionReceipt) /* 3 */
                .then((responseCode) => {
                    if (responseCode === 0) {
                        // Provision your user with their digital goods bought. 
                    }
                })
                .catch((e) => console.log(e));
        }
        else {
            applicationSettings.setBoolean(transaction.productIdentifier, true); /* 4 */
        }
    }    
});    

A nice way to store locally what products are purchased is to use NativeScript’s application-settings module. This way you can add a key in the application settings for any product bought by the user and then you can enable/disable specific parts of your application depending on what the user has purchased. So in lines /* 1 */, after a purchase is restored, and on /* 4 */, when the user has purchased a product, we set the product id in the application settings. The special case for product2 on line /* 2 */ is because we will be setting up product2 as a consumable purchase. There are differences of how consumable purchases work on iOS and on Android. For iOS consumable purchases are automatically consumed after a successful purchase, so the user can make another one right away. For Android you are responsible for consuming a purchase and flag it for repurchase. That’s why on line /* 3 */ we call the consumePurchase() method. If the consume is successful and the return code is 0 you should provision your users with the digital goods corresponding to the product bought. Note that for iOS this method always returns response code 0.
In case you have many consumable purchases you can add some suffix to the product id (for example .consumable) so you can easily distinguish for what products you should call the consumePurchase() method.
In this event handler you also have access to various other properties of the transaction which you can use (for example for extended verification of the transaction on your backend server with Apple/Google). You can check the full list of available properties here.

Finally we should add our handlers for the restore button tap and the tap on the product that should trigger a purchase:

export function onProductTap(data: ItemEventData) {
    let product = viewModel.get("items")[data.index] as Product;

    purchase.buyProduct(product);
}

export function onRestoreTap() {
    purchase.restorePurchases();
}

NB: For the buyProduct() method it is really important that you pass the product instance as returned by the getProducts() method. If you just create a new product and populate its properties the method will fail on iOS!

Up next

If you go ahead and run the code now, you should get a nice empty list 🙂 This is because we need to configure the products on iTunes Connect and Google Play Store.
In the next part of this tutorial I will guide through the setup on iTunes Connect and then we will run our sample application on iOS and make some sandbox purchases! So stay tuned!

Using NativeScript’s ImageCache to cache images and display them in list like controls

In this article I will review one of the very common usage you encounter when developing a mobile application that shows a list of images (i.e. an image gallery) and how you can use {N}’s ImageCache object to enhance the user experience by caching all images so they are loaded only once.

But let us start with the initial sample code. First we will create a class that we will bind for each image in the list:

import observable = require("data/observable");

export class ImageItem extends observable.Observable
{
    constructor(imageSrc : string)
    {
        super();
        this.set("imageSrc", imageSrc);
    }
}

Then we create a very simple page that will display the images:

<Page xmlns="http://schemas.nativescript.org/tns.xsd" 
      navigatingTo="navigatingTo">
    <ListView items="{{ images }}">
      <ListView.itemTemplate>
        <GridLayout>
          <Image src="{{ imageSrc }}" stretch="aspectFill" height="100"/>
        </GridLayout>
      </ListView.itemTemplate>
    </ListView>
</Page>

For simplicity of the example I use the builtin ListView widget, but in case you are creating a real photo gallery you should better use my GridView widget or the ListView widget from Telerik UI for NativeScript.

In the script for the page we have:

import observableArray = require("data/observable-array");
import observable = require("data/observable");
import imageItem = require("./image-item");
import pages = require("ui/page");

export function navigatingTo(args: pages.NavigatedData)
{
    var page = <pages.Page>args.object;
    var model = new observable.Observable();
    var images = new observableArray.ObservableArray<imageItem.ImageItem>();

    images.push(new imageItem.ImageItem("http://foo.com/bar1.jpg"));
    images.push(new imageItem.ImageItem("http://foo.com/bar2.jpg"));
    // ...
    images.push(new imageItem.ImageItem("http://foo.com/bar100.jpg"));

    model.set("images", images);
    page.bindingContext = model;
}

If we try this sample now you will notice that when you scroll down/up each image is downloaded every time it comes into visible range. And if the images are big this makes the ListView looks unresponsive (not to mention that if your user is using cellular data they wont be very happy :)).

Now we will enhance this by adding ImageCache, so once the image is loaded, subsequent loads will happen from the cache. In order to do so we will change a bit the implementation of the ImageItem class:

import observable = require("data/observable");
import imageCache = require("ui/image-cache");
import imageSource = require("image-source");

/* 1 */
var cache = new imageCache.Cache();
cache.maxRequests = 10;
cache.placeholder = imageSource.fromFile("~/images/no-image.png"));

export class ImageItem extends observable.Observable
{
    private _imageSrc: string
    get imageSrc(): imageSource.ImageSource
    {
        var image = cache.get(this._imageSrc); /* 2 */

        if (image)
        {
            return image; /* 3 */
        }

        cache.push(
            {
                key: this._imageSrc
                , url: this._imageSrc
                , completed:
                (image) =>
                {
                    this.notify(
                        {
                            object: this
                            , eventName: observable.Observable.propertyChangeEvent
                            , propertyName: "imageSrc"
                            , value: image
                        }); /* 4 */
                }
            });

        return cache.placeholder; /* 5 */
    }

    constructor(imageSrc : string)
    {
        super();
        this._imageSrc = imageSrc; /* 6 */
    }
}

With /* 1 */ we create an ImageCache object that is global and will be used for all ImageItems. We set up how many simultaneous downloads we allow and what image to display while the image is being loading. We changed the constructor so that instead of using Observable.set method we introduce a private class member that will contain the URL for the image and we initialize it with /* 6 */. Then we add a property imageSrc that first tries to pull the image from the cache with /* 2 */. If this returns an image then we directly return it with /* 3 */. If an image was not returned then it was not cached and we need to cache it first. For key in the cache we use the URL of the image as it uniquely identifies it. Because the cache loads the image asynchronously, while the image is being loaded we return with /* 5 */ the placeholder image that the user will see until the real image has been downloaded. Once the cache completes loading the image with /* 4 */ we notify all bound widgets that the imageSrc property was changed and they should pull the new data. What this will do is force each Image in the ListView to read the imageSrc property again and this time, because the image has been cached, the real image will be returned with /* 3 */.

By using this approach each image will be downloaded only once and any subsequent loads will not make any network traffic and will be returned instantly from the ImageCache.

Unofficial BBC News feed reader app created with NativeScript in couple of hours

Last week Jen Looper from Telerik set a challenge to create an unofficial BBC News reader app built with {N}. After reviewing another app built with ReactNative, I was surprised at how unorganized RN code looked. There is an XCode project and also have some JS styles embedded in the JS.

Knowing that {N} code is much cleaner (as it separates styles and logic) and I really loved the challenge to transform XML to native controls I decided to start on a freetime project and see where it goes. And here is the result:
ios

The Code

Lets start with the simple part first – the main screen that shows the available news items.

<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="onNavigatingTo">
    <ActionBar title="Feed" backgroundColor="#BB1919" color="#FFFFFF" />

    <GridLayout>
        <ListView items="{{ items }}" itemTap="goToItem" separatorColor="#BB1919">
            <ListView.itemTemplate>
                <GridLayout rows="150,auto,auto" columns="*,*" class="Container">
                    <Image row="0" col="0" colSpan="2" src="{{ imageHref }}" stretch="aspectFill" />
                    <Label row="1" col="0" colSpan="2" class="Title" text="{{ title }}" />
                    <Label row="2" col="0" class="Date" text="{{ lastUpdateDate | diffFormat() }}" /> <!-- 1 -->
                    <Label row="2" col="1" class="Category" text="{{ category.name }}" />
                </GridLayout>
            </ListView.itemTemplate>
        </ListView>
        <ActivityIndicator busy="{{ isLoadingIn }}" />
    </GridLayout>
</Page>

Nothing much special here – we put an ActionBar, style it and define a ListView and a template for it to display the news items. There is one thing worth mentioning – the formatting of the last update <!-- 1 -->. Here I’m using a converter to format the date. The converter is defined globally in the app.ts file so it can be used by all views in the application:

import application = require("application");
import moment = require("moment");

application.resources.diffFormat = (value: Date) => {
    let x = moment(value);
    let y = moment();
    let diffMins = y.diff(x, "minutes");
    let diffHrs = y.diff(x, "hours");

    if (diffMins < 60) {
        return `${diffMins} minutes ago`;
    }
    else {
        return `${diffHrs} hour${(diffHrs > 1 ? "s" : "" )} ago`;
    }
}

But wait isn’t moment a JS library, how are we using it for a native app? The cool thing is that because NativeScript is based on JS you can use many JS libraries out of the box (as long as they do not use anything browser/node specific). Neat, huh?

Now for the interesting part – the view that shows the content of the news item. If you look at the feed-item view and model files there is not much happening there. That’s because the main logic for this is hidden in the libs/parse-helper.ts files. First let me say that {N}’s XML Parser traverses the XML tree in depth. So the best shot we have to map those XML elements to NativeScript controls is to use a stack (or in the JS world – a simple array). So the general idea is when we encounter a start element we create an appropriate {N} object and add it to the stack. So for example paragraphs/crossheads I map to Labels, bold/italic text will be represented as Spans inside the Label, links will also be represented by Spans, but with a special styling and so on. Then when we get to some text depending on what we have at the top of the stack we add the text to that element. Finally when we get to an XML end element we pop one item from the stack and add it to the next one.

private static _handleStartElement(elementName: string, attr: any) {
    let structureTop = ParseHelper.structure[ParseHelper.structure.length - 1];

    switch (elementName) {
        // ...
        case "bold":
            let sb: Span;
            if (structureTop instanceof Span) { /* 1 */
                sb = structureTop;
            }
            else {
                sb = new Span();
                ParseHelper.structure.push(sb);
            }

            sb.fontAttributes = sb.fontAttributes | enums.FontAttributes.Bold;
            break; 

        case "link":
            if (!ParseHelper._urls) {
                ParseHelper._urls = [];
            }
            let link = new Span();
            link.underline = 1;
            link.foregroundColor = new Color("#BB1919");
            ParseHelper.structure.push(link);
            ParseHelper._urls.push({start: (<Label>structureTop).formattedText.toString().length}); /* 2 */
            break;

        case "url": /* 3 */
            let lastUrl = ParseHelper._urls[ParseHelper._urls.length - 1];
            lastUrl.platform = attr.platform;
            lastUrl.href = attr.href;
            break;

        case "caption":
            ParseHelper._isCaptionIn = true; /* 4 */
            break;

        // ...

        case "video":
            let videoSubView = 
                builder.load(fs.path.join(fs.knownFolders.currentApp().path, "view/video-sub-view.xml")); /* 5 */
            let model = ParseHelper._getVideoModel(attr.id);
            videoSubView.bindingContext = model;
            ParseHelper.structure.push(videoSubView);
            break;

        // ...
    }
}
private static _handleEndElement(elementName: string) {
    switch (elementName) {
        // ...
        case "paragraph":
        case "listItem":
        case "crosshead":
            let label: Label = ParseHelper.structure.pop();
            if (ParseHelper._urls) { /* 6 */
                label.bindingContext = ParseHelper._urls.slice();
                ParseHelper._urls = null;
            }
            (<StackLayout>ParseHelper.structure[ParseHelper.structure.length - 1]).addChild(label);
            break;  

        // ...

        case "italic":
        case "bold":
        case "link":
            // Added check for nested bold/italic tags
            if (ParseHelper.structure[ParseHelper.structure.length - 1] instanceof Span) { /* 7 */
                let link: Span = ParseHelper.structure.pop();
                (<Label>ParseHelper.structure[ParseHelper.structure.length - 1]).formattedText.spans.push(link);
            }
            break;

        case "caption":
            ParseHelper._isCaptionIn = false;
            break;      
        // ...
    }
}
private static _handleText(text: string) {
    if (text.trim() === "") return;

    let structureTop = ParseHelper.structure[ParseHelper.structure.length - 1];

    if (structureTop instanceof Label) {
        let span = new Span();
        span.text = text;
        (<Label>structureTop).formattedText.spans.push(span);
    }
    else if (structureTop instanceof Span) {
        (<Span>structureTop).text = text;
        if (ParseHelper._isCaptionIn) { /* 8 */
            ParseHelper._urls[ParseHelper._urls.length - 1].length = text.length;
        }
    }
    else {
        console.log("UNKNOWN TOP", structureTop);
    }
}

Couple of things worth mentioning:

  1. Since we can have tested bold and italic formatting I had to add /*1*/ in order not add multiple spans but instead apply the formatting on the previous span. And also /*7*/ which pops from the stack only if the item is a Span. In case of nested formatting the Span would be inserted to the Label on the first end bold/italic XML element.
  2. For links since we use simple text we need to remember on what positions exactly do links show (/*2*/) what are their properties (/*3*/) and what is the length of the text in the link (/*4*/ and /*8*/). Then once we finish parsing all the items for the Label we set the found urls as binding context for the Label (/*6*/)
  3. For the video I decided to take a different approach. Because for the video we will need a more complex layout – because we have poster image, play button image and then when clicked we should load and show the video, I decided to separate this in a separate file video-sub-view.xml:
<GridLayout height="200" tap="{{ startVideo }}">
    <Image stretch="aspectFill" src="{{ posterHref }}" />
    <Image src="~/images/play-button.png" width="100" stretch="aspectFit" height="200" />
    <ActivityIndicator busy="{{ isLoadingIn }}" />
</GridLayout>

Then on <!--5--> I load the file with the built-in functions provided by {N}. The neat part is that this function returns a View object which is basically the base building block for all controls. And with that view you can do whatever you can do with any other {N} control. In this case I set the bidningContext. The only catch is that the builder requires the full path to load the XML file. So you cannot use relative paths but you must first get the application directory and then add to that the path to your file. For showing and playing the actual video I’m using Brad Martin’s nativescript-videoplayer plugin.

Conclusion

With only a couple of hours {N} allowed me to create a fully functional native app that works seamlessly for both iOS and android.
You can find the full code here.
You can find more about the challenge and other entries here

The dreaded NativeScript 2.0 Android plugin problem and why it will affect everyone not just plugin developers

UPDATE (04/17/2016): Our prayers have been heard and the {N} team decided to continue support of the current plugin structure with AndroidManifest.xml in the plugin’s platforms folder in addition to the new .aar structure. You can read more about it here.


As a {N} plugin developer last week it was brought to my attention that there are major breaking changes coming to the platforms/android folder. You will probably think that as a plugin user this does not affect you in any way. Well actually it does affect you and I will outline below why it probably be a breaking change for any plugins you use that have some platform specific permissions/resources for android.

Let’s first look at what this change is all about: The {N} core team decided to remove the current ability for plugin developers to include required changes to AndroidManifest.xml and/or used resources in the res folder. And instead of that the ONLY way they added for us to do that is via a precompiled .aar file which should be distributed with the plugin.
But why is this such a problem for plugin developers? As an example I will use the nativescript-contacts plugin to which I contributed.

Workflow pre-2.0

This plugin requires some permissions on android to write and read contacts. Initially it was only reading contacts so in its manifest file there was only the READ_CONTACTS permission. I then added saving of contacts which required two more additional permissions WRITE_CONTACTS and GET_ACCOUNTS. So after I wrote the actual code for saving all I had to do was just to add the appropriate XML in the AndroidManifest.xml and submit the pull request. Then the repo owner would clearly see what this change is and why it was needed.

Workflow 2.0+

So I just finished implementing the logic and need to add the appropriate permissions so the plugin is Plug’n’Play for any plugin end-user. In order to do that first the repo owner would have to have created a separate android project that is setup to be compiled as an android library – it can be either in some other repo or in most cases in the same repo but in some separate folder. Ok, I manage to somehow find where the AndroidManifest.xml file is and add the needed permissions. But wait I’m not done yet. Remember we need an .aar file. So in order to get that file I need to compile this project first. Since I don’t have Android Studio we will use simple gradle to compile. So we have the long sought .aar file. But we are not finished yet. This .aar file is located in the build folder of the other project. So we need to move it under the platforms/android folder of our plugin. Now we are finally done and we can submit our PR to the owner. But the work is not yet finished. Now the repo owner needs to verify my pull request. He checks the code and all looks ok, then comes the .aar file. Ok since it is a binary file he sees that it was changed, but what exactly was the change compared to the previous one? Difficult to find out. He would only assume that I did not do anything wrong or bad. You can see what I’m talking about in the nativescript-calendar plugin which was updated to the new and “better” structure.

So to sum it up: for adding up 2 lines in the AndroidManifest.xml file I additionally had to perform manually 4 steps:
1. Find the manifest file “hidden” inside a different folder structure
2. Build the separate project
3. Find the .aar file in the build folder of the separate project
4. Replace the .aar file in the correct plugin folder

In the era where we have self-driving cars, I think that’s a bit to manually 🙂 Not to mention the overhead it creates from developer point of view.

How does this work with other similar frameworks?

For cordova the plugin has a special config.xml file which can include manifest file additions.
For ReactNative developers have special folders where they can put the plugin’s native iOS/Android code and files.

Conclusion

There has been a big discussion about this issue with many plugin authors trying to explain the situation I outlined above and proposing different solutions. I even ended up submitting a PR with one possible fix with the tradeoff to make the build a bit slower. But all of this just hit a brick wall and came to no avail with no reasonable explanation.

So many plugin developers will remove the manifest file to make their plugins compatible for 2.0 and will write in the readme of the plugin that users need to add this and that to their application manifest file. This is where it affects you as an end-user of a plugin. If you oversee these details in the readme (or if you are a Telerik Platform user and add your plugins via the AppBuilder or Visual Studio interface) the plugin will just not work and your application will most probably crash. At this point send all your love to NativeScript 🙂