Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Build An RSS Reader Mobile App With NativeScript And Angular

Rich site summary (RSS) feeds are very common on blogs or sites that have a publication type feed. These feeds are in XML format and have information such as the publication title, a summary, or other bits of information that is rather useful. There are plenty of mobile applications on Google Play and iTunes that support the aggregation of RSS feeds, but have you ever wanted to build your own? Maybe you want to create your own news application based on your own algorithms, or maybe your company has a mobile application and you’d like to include a blog section to the mobile app. Whatever the need may be, doing so is not difficult.

We’re going to see how to use Angular, Yahoo’s YQL language, and NativeScript to build a fully native RSS feed reader for Android and iOS.

Before we get too far ahead of ourselves, it is probably a good idea to see what we’re going to build. We’re going to build an application that supports adding news feeds to a database and displaying them in a list. It will actually look like this:

NativeScript RSS Feed Reader

In the above animated image we can see our application only has two screens. What we don’t see is the database being used and the development frameworks. Because JavaScript plays fabulously with JSON, we’re going to use a JSON database. Our database will be the open source NoSQL database, Couchbase. For development, we’ll be using Angular and TypeScript.

The Requirements

There are a few requirements that must be met before attempting to develop this application.

  • Node.js 4.0 or higher
  • The Android SDK and / or Apple’s Xcode
  • NativeScript 2.4 or higher

We need Node.js because it ships with the Node Package Manager (NPM) which is used for installing NativeScript. To be successful with this application which uses Angular, we need to use the latest NativeScript version, which at the time of writing this is 2.4. When it comes to building the application, you need the Android SDK for building for Android and Xcode for building for iOS. You can have one or both installed, it doesn’t matter since our application will work for both.

Creating a New NativeScript Android and iOS Application

To make this tutorial easy to understand, we’re going to start the project from scratch. From the Command Prompt (Windows) or Terminal (Mac and Linux), execute the following:

tns create rss-feed-reader --ng
cd rss-feed-reader
tns platform add ios
tns platform add android

A few things to note in the above commands. The --ng tag tells us we are going to create an Angular project. Also note that, like mentioned previously, you need Xcode or the Android SDK when it comes to building for specific platforms.

Adding the Project Plugins and Dependencies

This project does have an external dependency. We will be using Couchbase NoSQL for storing our JSON data, so we need to download the NativeScript Couchbase plugin.

To install this plugin in your project, execute the following:

tns plugin add nativescript-couchbase

While the above command will install the plugin, it will not link the TypeScript type definitions necessary for our project. To link the TypeScript definitions, open the project’s references.d.ts file and add the following line:

/// <reference path="./node_modules/nativescript-couchbase/couchbase.d.ts" />

At this point the project and its dependencies are ready for development. However, it is probably a smart idea to learn about our data model first.

Understanding the NoSQL Data Model

We’re going to be using NoSQL in this particular project which may be new to you. NoSQL is relatively new to the mobile scene, but very popular when it comes to servers. We’re not going to be using any servers in this application.

This particular application will have only a single JSON document type. We are only going to be storing feed sources, not the data that resides in those sources. Instead we’ll be loading that data live. With that said, our single document type will look like the following:

{
    "type": "source",
    "link": "https://www.thepolyglotdeveloper.com/rss"
}

We don’t name NoSQL documents like we would an RDBMS table, but let’s refer to this document as a source document. It will contain a type property and a link property, both which I made up. They are not reserved words.

Couchbase Lite MapReduce Views for Querying Source Data

In case NoSQL is new to you, querying in NoSQL will be too. Instead of executing SQL queries like you would in SQLite, you execute a query against what is called a MapReduce view.

These MapReduce views are functions that look like the following:

function(document, emit) {
    if(document.type) {
        emit(document._id, document);
    }
}

Of course the above view is only an example and isn’t how you would create one in NativeScript, the logic is correct. If querying the above example, a key-value pair would be returned for any document that has a type property. The key would be the document id and the value would be the full document.

We’ll explore more on this later.

Creating the Couchbase Singleton Provider

To get the most out of our database and do so in an elegant way, we should create a singleton instance of it to be used on every page of the application.

Create and open app/couchbase.ts within your project and include the following TypeScript code:

import {Couchbase} from 'nativescript-couchbase';

export class CouchbaseInstance {

    private database: any;

    public constructor() {
        if(!this.database) {
            this.database = new Couchbase("rss-database");
            this.database.createView("sources", "1", (document, emitter) => {
                if(document.hasOwnProperty("type") && document.type == "source") {
                    emitter.emit(document._id, document);
                }
            });
        }
    }

    public getDatabase() {
        return this.database;
    }

}

So what is happening in the above singleton?

First we are importing the plugin that we downloaded previously. Inside the CouchbaseInstance class we have a private database variable which will hold our open database instance.

Inside the constructor method we get a local database called rss-database and create the view that will be used for querying. The view only needs to be created once. Every time the view changes you will need to increase the version number, otherwise the changes will be ignored. The view will emit data only if the document has a property called type and it equals source. The database will only open if it hasn’t already been opened.

Finally we have a function called getDatabase for returning the open database instance.

Creating a Component for Viewing Feed Data from a Source

We’re going to work on the first of our two components now. To keep the flow, we’re going to work on the components in the order that they show up in the application lifecycle. This means we’re going to start with the component for showing a list of feed items.

First we need to create the appropriate directories and files. I’m going to do this from the Terminal by executing the following:

mkdir -p app/components/feed
touch app/components/feed/feed.component.ts
touch app/components/feed/feed.component.html
touch app/components/feed/feed.component.css

Feel free to create the above directories and files manually if the Terminal commands don’t work for you or if they aren’t your preference.

Of the files created, the TypeScript file will hold all our component logic, the HTML file will hold all our UI and the CSS file will hold all our stylesheet information. We’re going to start by editing the component logic.

Open the project’s app/components/feed/feed.component.ts file and include the following code. We’ll break it down after.

import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Location } from "@angular/common";
import { Http, Request, RequestMethod } from "@angular/http";
import * as Utility from "utils/utils";
import { CouchbaseInstance } from "../../couchbase";
import "rxjs/Rx";

@Component({
    selector: "feed",
    templateUrl: "./components/feed/feed.component.html",
    styleUrls: ["./components/feed/feed.component.css"]
})
export class FeedComponent implements OnInit {

    private database: any;
    public feedList: Array<Object>;

    public constructor(private router: Router, private location: Location, private http: Http, private couchbaseInstance: CouchbaseInstance) {
        this.feedList = [];
        this.database = this.couchbaseInstance.getDatabase();
    }

    public ngOnInit() {
        this.location.subscribe((path) => {
            this.load();
        });
        this.load();
    }

    private load() {
        this.feedList = [];
        var rows = this.database.executeQuery("sources", {descending: false});
        for(let i = 0; i < rows.length; i++) {
            this.http.request(new Request({
                method: RequestMethod.Get,
                url: "https://query.yahooapis.com/v1/public/yql?q=select%20title%2C%20description%2C%20link%20from%20rss%20where%20url%3D%22" + rows[i].link + "%22&format=json&diagnostics=true&callback="
            }))
            .map(result => result.json())
            .subscribe((result) => {
                let items = result.query.results.item;
                for(let i = 0; i < items.length; i++) {
                    items[i].description = this.cleanText(items[i].description);
                    items[i].description = items[i].description.substring(0, items[i].description.indexOf("...") + 3);
                }
                this.feedList = this.feedList.concat(items);
            }, (error) => {
                console.log(error);
            });
        }
    }

    private cleanText(text: string) {
        let cleaned = text;
        cleaned = cleaned.replace(/(<([^>]+)>)/ig,"");
        cleaned = cleaned.replace(/&#8217;/gi, "\'");
        cleaned = cleaned.replace(/&#039;/gi, "\'");
        cleaned = cleaned.replace(/\[&#8230;\]/gi, "...");
        return cleaned;
    }

    public navigateToSources() {
        this.router.navigate(["sources"]);
    }

    public open(item: any) {
        Utility.openUrl(item.link);
    }

}

So let’s break down the above TypeScript code. The first thing we see is a bunch of import statements.

import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Location } from "@angular/common";
import { Http, Request, RequestMethod } from "@angular/http";
import * as Utility from "utils/utils";
import { CouchbaseInstance } from "../../couchbase";
import "rxjs/Rx";

We are importing standard Angular and NativeScript components, but we are also importing the CouchbaseInstance singleton that we created and a Utility module for working with the web browser.

In the @Component chunk we are pairing the HTML and CSS files to this particular TypeScript file. Pretty standard Angular stuff up until this point.

Now we get into the FeedComponent class. We have two variables, one of which is public. The public feedList variable can be accessed from the HTML UI. This variable will contain all the feed data.

public constructor(private router: Router, private location: Location, private http: Http, private couchbaseInstance: CouchbaseInstance) {
    this.feedList = [];
    this.database = this.couchbaseInstance.getDatabase();
}

This brings us to the constructor method where we initialize the list and get the shared database instance.

public ngOnInit() {
    this.location.subscribe((path) => {
        this.load();
    });
    this.load();
}

It is not a good idea to load data in the constructor method. Instead we should do our data loading in the ngOnInit which happens after the constructor is fired. We also use the location.subscribe chunk to track pop events in the navigation stack. In other words, when the back button is pressed, the load method will be called

The load method has been called in two different locations, so now we can figure out what is happening:

private load() {
    this.feedList = [];
    var rows = this.database.executeQuery("sources", {descending: false});
    for(let i = 0; i < rows.length; i++) {
        this.http.request(new Request({
            method: RequestMethod.Get,
            url: "https://query.yahooapis.com/v1/public/yql?q=select%20title%2C%20description%2C%20link%20from%20rss%20where%20url%3D%22" + rows[i].link + "%22&format=json&diagnostics=true&callback="
        }))
        .map(result => result.json())
        .subscribe((result) => {
            let items = result.query.results.item;
            for(let i = 0; i < items.length; i++) {
                items[i].description = this.cleanText(items[i].description);
                items[i].description = items[i].description.substring(0, items[i].description.indexOf("...") + 3);
            }
            this.feedList = this.feedList.concat(items);
        }, (error) => {
            console.log(error);
        });
    }
}

Remember that Couchbase MapReduce view that we created? Now we’re actually querying it. For each of the results we are going to make an HTTP request against Yahoo’s YQL service.

Wait a second. What is Yahoo’s YQL service?

I actually came across this service from a friend of mine, Raymond Camden. As you probably know, RSS is in XML format which is a total pain to work with in JavaScript. It makes sense to convert XML to JSON before trying to use it with JavaScript. It makes even more sense to convert it server side as it can be expensive to do via a mobile application. This is what YQL does.

A YQL query for an RSS feed looks like this:

select title, description, link from rss where url="https://www.engadget.com/rss.xml"

The above query will go against the Engadget RSS feed and extract only the title, description, and link tags and convert them to JSON.

You’ll notice we are passing our results into a cleanText function. This is because we may end up receiving encoded characters which isn’t exactly pleasant to display on the screen. This function looks like the following:

private cleanText(text: string) {
    let cleaned = text;
    cleaned = cleaned.replace(/(<([^>]+)>)/ig,"");
    cleaned = cleaned.replace(/&#8217;/gi, "\'");
    cleaned = cleaned.replace(/&#039;/gi, "\'");
    cleaned = cleaned.replace(/\[&#8230;\]/gi, "...");
    return cleaned;
}

This leaves us with the navigateToSources method which will take us to the next component and the open method that will open links in the web browser. The link to be opened by the open method is passed in from the HTML UI via a tap event.

This brings us the the UI that goes with the TypeScript logic. Open the project’s app/components/feed/feed.component.html file and include the following markup:

<ActionBar title="RSS Feed">
    <ActionItem text="Add" (tap)="navigateToSources()" ios.position="right"></ActionItem>
</ActionBar>
<GridLayout>
    <ListView [items]="feedList">
        <template let-item="item">
            <StackLayout (tap)="open(item)">
                <Label [text]="item.title" class="title" textWrap="true"></Label>
                <Label [text]="item.description" class="description" textWrap="true"></Label>
            </StackLayout>
        </template>
    </ListView>
</GridLayout>

In the above XML we have an application bar with a title and a single button. The button will navigate us to the next component when pressed. The core content is a list view that is bound to the public feedList variable from the TypeScript file. This array is looped and each item is presented on the screen.

Out of the box though, the UI looks a bit plain. We’re going to give it some flare by working with the CSS stylesheet. Open the project’s app/components/feed/feed.component.css file and include the following:

Stacklayout {
    padding: 15;
}

.title {
    font-weight: bold;
    font-size: 18;
}

The above is pretty standard CSS, like you would find in web design.

Creating a Component for Adding RSS Sources

This brings us to our second and final component in the RSS feed reader application. We need to be able to add sources to the database.

We’ll start by creating the necessary directories and files for this component. I’m going to create them from the Terminal like so:

mkdir -p app/components/sources
touch app/components/sources/sources.component.ts
touch app/components/sources/sources.component.html
touch app/components/sources/sources.component.css

Again, if you’re using Windows, or don’t have the above commands, creating them manually is fine too.

Just like with the previous component we have a TypeScript file for logic, HTML file for UI, and a CSS file for theming information. We’re going to start with the TypeScript file again.

Open the project’s app/components/sources/sources.component.ts file and include the following code:

import { Component, OnInit } from "@angular/core";
import { Router } from "@angular/router";
import { Location } from "@angular/common";
import * as Dialogs from "ui/dialogs";
import { CouchbaseInstance } from "../../couchbase";

@Component({
    selector: "sources",
    templateUrl: "./components/sources/sources.component.html",
    styleUrls: ["./components/sources/sources.component.css"]
})
export class SourcesComponent implements OnInit {

    private database: any;
    public sourceList: Array<any>;

    public constructor(private router: Router, private location: Location, private couchbaseInstance: CouchbaseInstance) {
        this.sourceList = [];
        this.database = this.couchbaseInstance.getDatabase();
    }

    public ngOnInit() {
        var rows = this.database.executeQuery("sources", {descending: false});
        this.sourceList = rows;
    }

    public add() {
        Dialogs.prompt("Input an RSS URL", "http://").then((result) => {
            if(result.result == true && result.text != "http://" && result.text != "https://") {
                this.database.createDocument({
                    "type": "source",
                    "link": result.text
                });
                this.sourceList.push({"type": "source", "link": result.text});
            }
        });
    }

    public tapActions(item: any) {
        Dialogs.action("Choose an Action", "Cancel", ["Delete"]).then((result) => {
            if(result == "Delete") {
                this.database.deleteDocument(item._id);
                this.location.back();
            }
        });
    }

}

So what is happening in the above code?

Just like in the first component we are importing a bunch of NativeScript and Angular components and pairing the TypeScript file with an HTML and CSS file. The real magic happens in the SourcesComponent class.

We have a public variable that will be bound to the UI. It is initialized in the constructor method along with obtaining the database instance.

public ngOnInit() {
    var rows = this.database.executeQuery("sources", {descending: false});
    this.sourceList = rows;
}

In the above ngOnInit method we are loading the source data from the database by querying our view.

When it comes to adding new sources, we’re going to take the easy way out and use a dialog prompt instead of creating a whole new component for the job.

public add() {
    Dialogs.prompt("Input an RSS URL", "http://").then((result) => {
        if(result.result == true && result.text != "http://" && result.text != "https://") {
            this.database.createDocument({
                "type": "source",
                "link": result.text
            });
            this.sourceList.push({"type": "source", "link": result.text});
        }
    });
}

When the add method is called, the prompt will show. If data was entered into the prompt, it will be saved to the database. The data of each NoSQL document of course includes the document type and the link that was provided. After adding the link, it is also added to the UI list.

Finally we have the tapActions method which will show a dialog:

public tapActions(item: any) {
    Dialogs.action("Choose an Action", "Cancel", ["Delete"]).then((result) => {
        if(result == "Delete") {
            this.database.deleteDocument(item._id);
            this.location.back();
        }
    });
}

We’re doing the above because we want a way to be able to delete sources from the database. When the user chooses to delete, the document is deleted and we are navigated back to the feed list component.

This brings us into the HTML file that goes with this component. Open the project’s app/components/sources/sources.component.html file and include the following markup:

<ActionBar title="RSS Sources">
    <NavigationButton text="Back"></NavigationButton>
    <ActionItem text="Add" (tap)="add()" ios.position="right"></ActionItem>
</ActionBar>
<GridLayout>
    <ListView [items]="sourceList">
        <template let-item="item">
            <StackLayout (tap)="tapActions(item)">
                <Label [text]="item.link"></Label>
            </StackLayout>
        </template>
    </ListView>
</GridLayout>

You’ll notice the above XML looks similar to the last HTML file. In the above, we are including an override to the NavigationButton so we can define our own text. We are also including a tap event in the list view.

The CSS that goes with the HTML can be seen in the project’s app/components/sources/sources.component.css file:

Stacklayout {
    padding: 15;
}

Our components are done, but we’re not in the clear yet. We need to bring them together.

Bring the Components Together with the Angular Router

To bring the components together we need to create an Angular router. Doing this requires changes to the project’s app/app.module.ts and app/app.component.ts files as well as creating a new file.

We’re going to start by altering the project’s app/app.component.ts file. Open it and include the following:

import { Component } from "@angular/core";

@Component({
    selector: "my-app",
    template: "<page-router-outlet></page-router-outlet>",
})
export class AppComponent { }

In the above code we are defining our component template as the page-router-outlet. In short, this file acts as our driver and main entry point to the application routes.

Now we need to define the routes we wish to use during navigation. Create and open an app/app.routing.ts file in your project, and include the following code:

import { FeedComponent } from "./components/feed/feed.component";
import { SourcesComponent } from "./components/sources/sources.component";

export const AppRoutes: any = [
    { path: "", component: FeedComponent },
    { path: "sources", component: SourcesComponent }
];

export const AppComponents: any = [
    FeedComponent,
    SourcesComponent
];

In the above code we are importing every possible route and adding them to an array of defined paths. The empty path represents the default route and the first screen that will show in our application. As a convenience to the next step, we add all available routes to an array.

To load the routes we need to look in the app/app.module.ts file. Open it and include the following code:

import { NgModule, NO_ERRORS_SCHEMA } from "@angular/core";
import { NativeScriptModule } from "nativescript-angular/nativescript.module";
import { NativeScriptFormsModule } from "nativescript-angular/forms";
import { NativeScriptHttpModule } from "nativescript-angular/http";
import { NativeScriptRouterModule } from "nativescript-angular/router";

import { AppRoutes, AppComponents } from "./app.routing";
import { AppComponent } from "./app.component";
import { CouchbaseInstance } from "./couchbase";

@NgModule({
    declarations: [AppComponent, ...AppComponents],
    bootstrap: [AppComponent],
    imports: [
        NativeScriptModule,
        NativeScriptFormsModule,
        NativeScriptHttpModule,
        NativeScriptRouterModule,
        NativeScriptRouterModule.forRoot(AppRoutes)
    ],
    providers: [
        CouchbaseInstance
    ],
    schemas: [NO_ERRORS_SCHEMA]
})
export class AppModule { }

Here we’re doing quite a bit.

We’re importing the necessary Angular and NativeScript components, but we’re also importing the Couchbase singleton and our routing file.

The available components array that was created in the routing file is merged with the declarations array in the @NgModule block. The Couchbase service is added to the providers array and the various NativeScript modules are configured in the imports array.

At this point in time we can use Couchbase, HTTP requests, and navigation within our application.

Testing the RSS Reader Application

Congratulations, you should have a functional RSS feed reader application! If you’ve been following along, you should be able to test your application in an Android or iOS simulator.

If you’re developing for Android, execute the following:

tns emulate android

The above command will start an Android emulator if you have one installed and run the app. If you’re developing for iOS, execute the following to test on iOS:

tns emulate ios

Let’s say you want to see this application in action, but you don’t want to go through the whole tutorial. You can download the full project here and test it out for yourself.

After you download and extract the project, execute the following:

tns install

I stripped out all the dependencies to keep the download size small, so running the above command will download them again. Once the dependencies have been downloaded, you can proceed to emulating or installing the application.

Conclusion

You just saw how to develop an RSS feed reader application using NativeScript and Angular. We explored many different technologies and services in this example. For example we saw how to work with NoSQL and we saw how to use YQL for parsing XML data into JSON.

If you’re interested in downloading this project, you can download it here. I put a lot of effort into coming up with this example and writing the tutorial. I would like to request you do not share this project download.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in Java, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Apache Cordova. Nic writes about his development experiences related to making web and mobile development easier to understand.

Search

Follow Us

Support This Site