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 🙂