Last week I wrote up my initial thoughts on working with NativeScript. After working through the getting started guide, I thought I'd take a stab at building a simple app, a RSS reader. Before going further, note that you should assume my code is crap. It works - but I'm sure I've done things like - well - a noob. Because I am. So I'd think twice before using my code. (Although you are welcome to it - I'll have a link to the code at the end.)
My RSS reader consists of two screens - an initial list based on the entries from an RSS feed and a detail page for the actual blog entry. Here's the initial screen.

To be clear, that lovely red header there was me using my design chops. Don't blame NativeScript for that. Anyway, here's the detail view:

That's a lot of text (partially because that blog entry really does have a lot of text at first) and not terribly nice looking, but it works. Now let's take a look at the code.
First off, the home page view, which is really just a list.
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="loaded">
<Page.actionBar>
<ActionBar title="{{ title }}" />
</Page.actionBar>
<StackLayout orientation="vertical">
<ListView items="{{ rssList.feedItems }}" itemTap="loadItem">
<ListView.itemTemplate>
<Label text="{{ title }}" horizontalAlignment="left" verticalAlignment="center" />
</ListView.itemTemplate>
</ListView>
</StackLayout>
</Page>
Nothing too special here. You've got a list with a tap handler. On top you can see a call to loaded()
for when the page loads. Now let's look at the code behind this.
var RssListViewModel = require('../shared/view-models/rss-list-view-model');
var frameModule = require('ui/frame');
var config = require('../shared/config');
var Observable = require('data/observable').Observable;
var viewModule = require('ui/core/view');
var page;
var pageData = new Observable({
rssList:RssListViewModel,
title:config.title
});
exports.loaded = function(args) {
page = args.object;
page.bindingContext = pageData;
//RssListViewModel.empty();
RssListViewModel.load();
};
exports.loadItem = function(args) {
console.log('tap item',args.index);
console.log('tap item 2', args.view.bindingContext.title);
//rssList.viewModel.set('selectedItem', args.view.bindingContext);
RssListViewModel.viewModel.set('selectedItem', args.view.bindingContext);
frameModule.topmost().navigate('views/item-page');
}
So for the most part, this too is rather simple. Most of the logic is in a view module. This file basically handles asking the view model to do it's thing and return a list of RSS entries.
I do want to point out one thing. Notice in loadItem()
I call a set operation. This is how I handle "I'm leaving this view but want to remember what I clicked." This one thing took me roughly 70% of the development time for this project. Why? At first, I was creating an instance of my view model, not just requiring it. I did this on my detail page too. That meant when I set a value on it on the list page, I lost that when the object was recreated on the detail page. That seems trivial, but it took me forever to get around that.
I also discovered later that you can pass random data to another view via navigate. You can see that described here in the docs. I didn't see that at first because when I went to the API reference in the docs, I was initially on the "Module" for Frame and not the "Class" for it. I honestly don't know the difference (I just asked on Slack though so hopefully I'll get a clue ;).
Now let's look at the detail page.
<Page xmlns="http://schemas.nativescript.org/tns.xsd" navigatingTo="loaded">
<Page.actionBar>
<ActionBar title="{{ title }}" />
</Page.actionBar>
<StackLayout orientation="vertical">
<ScrollView>
<HtmlView html="{{ text }}" />
</ScrollView>
</StackLayout>
</Page>
Again - fairly simple. I first used a TextView and of course that doesn't render HTML. I did find odd performance issues with this control. The first few views worked perfect. Then I saw a noticeable lag in rendering the view. I'd say maybe 2-3 seconds. I'm fairly certain it is probably my code, but I've let me friends on the NativeScript code know about what I encountered. Ok, so now the code behind the view.
var RssListViewModel = require('../shared/view-models/rss-list-view-model');
var Observable = require('data/observable').Observable;
var pageData = new Observable({
title:"",
text:""
});
exports.loaded = function(args) {
page = args.object;
page.bindingContext = pageData;
console.log('loaded the item page');
console.log(RssListViewModel.viewModel.get('selectedItem').title);
pageData.title = RssListViewModel.viewModel.get('selectedItem').title;
pageData.text = RssListViewModel.viewModel.get('selectedItem').description;
}
Basically I ask for the data I saved in the previous view and update a local observable. I had tried to bind directly to my instance of RssListViewModel, but noticed that content only updated one time. Again - that's possibly my fault.
Finally, let's look at the view model. I used one of the methods I described in my blog post on the topic - Parsing RSS Feeds in JavaScript - Options. Just in case it isn't obvious - yes - I used something in NativeScript that worked perfectly fine for Cordova too. While I may struggle a bit with the UI of NativeScript and other aspects, being able to re-use stuff I've already learned in the hybrid space is a big win. Anyway, the code:
var observable = require('data/observable');
var ObservableArray = require('data/observable-array').ObservableArray;
var fetchModule = require('fetch');
var config = require('../config');
function handleErrors(response) {
if (!response.ok) {
console.log(JSON.stringify(response));
throw Error(response.statusText);
}
return response;
}
exports.empty = function() {
while (feedItems.length) {
feedItems.pop();
}
};
exports.load = function name(params) {
console.log('CALLING LOAD');
//handle caching
if(feedItems.length > 0) {
console.log('leaving early');
return;
}
return fetch('https://query.yahooapis.com/v1/public/yql?q=select%20title%2Clink%2Cdescription%20from%20rss%20where%20url%3D%22'+encodeURIComponent(config.rssURL)+'%22&format=json&diagnostics=true', {
})
.then(handleErrors)
.then(function(response) {
return response.json();
}).then(function(data) {
console.log('number of rss entries: '+data.query.results.item.length);
data.query.results.item.forEach(function(feedItem) {
feedItems.push({
title: feedItem.title,
link: feedItem.link,
description: feedItem.description
}
);
});
});
}
var feedItems = new ObservableArray();
exports.feedItems = feedItems;
var viewModel = new observable.Observable();
exports.viewModel = viewModel;
This is - I assume - mostly self-explanatory, but let me know in the comments below if anything isn't clear. There's one more file I didn't show yet - a simple config object I can use to quickly change the title of the app and the RSS feed:
module.exports = {
rssURL:"http://feeds.feedburner.com/raymondcamdensblog",
title:"Raymond Camden's Blog"
}
There's two things missing from this app that I'd like to correct. First, a good mobile application should recognize when it is offline. I need to update the app to notice that, let the user know, and possibly start working again once network connectivity is restored. Secondly, many RSS feeds only contain a small portion of the entry text. I'd like to add a button that would open the entry in the native browser for proper reading.
Want the complete code? (And again, remember that it is code being written by a noob. I'd hate to be accused of leading people to bad code.) You can find the complete source here: https://github.com/cfjedimaster/NativeScriptDemos/tree/master/rssTest1.