A few years back I wrote a series of blog entries (linked to at the bottom) that discussed building a simple RSS reader application with PhoneGap/Cordova. The application used two variables, a simple name and RSS url, to drive an application that would grab the RSS feed, make a list, and let you read individual entries in the app. The final version of the app made use of the (non-core at the time) ChildBrowser plugin to let you read the entry on the site itself. (This was especially useful for RSS feeds like mine that show partial content in RSS.) I decided to update this application to make use of Ionic. It isn't an incredibly complex app, but I thought folks would be interested in the update.

Update on June 22, 2015: I recently looked over this code and made some big changes. See the update here: An update to my RSS Reader built with Ionic

First, let's discuss some of the architecture changes at a high level.

  • The previous version of the application used jQuery Mobile. jQuery Mobile handed both the UI and the Single Page Architecture of the application. Now Ionic will handle that both. Ionic has its own UI and it uses Angular beneath the hood. Once again I'll remind folks - I'm an Angular noob.
  • The previous version of the application had no support for offline mode. It did have support for the RSS feed being done. It would check for a LocalStorage copy of the entries. For this version I decided to add "proper" offline checking but just ignore RSS errors. I was maybe being a bit lazy in that regards but I'm definitely happy with the offline check now. Currently it just gives you a nice error and - well - that's it. You can't say "Check Again" or some such. But it is definitely an improvement.
  • In terms of UI, I didn't get too complex. I think more could be done probably, but for now, I'm happy with it.
  • Yesterday I blogged about Ionic 1.2.0's new LiveReload feature. While it is very cool, it has an issue you should be aware of. Imagine your app, like mine, has an error state where it sends you to some path, like /error, and that's all you can do. When you test this under Ionic's LiveReload, as you edit your code the reload feature does a reload... on that URL. What I found was that I was "stuck" in /error and I couldn't get out. Now - for release - that's what I want. But I had expected LiveReload to reload the app as a whole, i.e. to go to /. To work around this, I added a few hacks to my code. I'll explain them when I get to the code. They are very short hacks so I won't bother removing them from the zip/GitHub repo, but you should be aware of why they are there. I filed a bug with Ionic on it: https://github.com/driftyco/ionic/issues/2144.

Ok, before we jump into the code, let's look at some screen shots!

On load, it uses an Ionic Loading widget while it checks for offline status and parses the RSS feed.

After everything is checked, the entries are then displayed using Ionic's List control.

Clicking an entry displays the RSS entry. Depending on the RSS URL, this may be a complete entry or a partial one. This - in particular - is where I think a bit more could be done to make it look nicer.

Finally, we use the now core plugin, InAppBrowser, to support reading the entry on the web site.

Ok, let's look at the code! (And yet another reminder - I'm new to Ionic and Angular.) First, the home page. Of special interest here is that I'm using ngCordova and Google's JavaScript API.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title>RSS Reader</title>

    <link href="lib/ionic/css/ionic.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">
	  
    <script src="lib/ionic/js/ionic.bundle.js"></script>
	<script src="lib/ng-cordova.js"></script>
	  
    <!-- cordova script (this will be a 404 during development) -->
    <script src="cordova.js"></script>

	<script type="text/javascript" src="https://www.google.com/jsapi"></script>

    <script src="js/app.js"></script>
    <script src="js/controllers.js"></script>
  </head>
  <body ng-app="starter">
	  
  	<ion-nav-bar class="bar-positive">
		<ion-nav-buttons side="left">
			<button class="button" ng-click="goHome()" ng-show="notHome">
			Home
			</button>
		</ion-nav-buttons>
	</ion-nav-bar>

  	<ion-nav-view>
	</ion-nav-view>

  </body>
</html>

Ok, now let's look at app.js. The big change here from core Ionic was to remove $ionicPlatform.ready from app.js. As I've blogged before, you have a race condition between Cordova's DeviceReady and your Angular app actually starting. So I decided to use my first controller as a way to bootstrap that particular issue.

(function() {
/* global angular,window,cordova,console */
	
	angular.module('starter', ['ionic','ngCordova','rssappControllers'])

	.config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) {

		$stateProvider
			.state('Home', {
				url: '/',
				controller: 'HomeCtrl', 
				templateUrl: 'partials/home.html'
			})
			.state('Entries', {
				url: '/entries', 
				controller: 'EntriesCtrl', 
				templateUrl: 'partials/entries.html',
			})
			.state('Entry', {
				url: '/entry/:index',
				controller: 'EntryCtrl', 
				templateUrl: 'partials/entry.html',
			})
			.state('Offline', {
				url: '/offline', 
				templateUrl: 'partials/offline.html'
			});

		$urlRouterProvider.otherwise("/");

	}])

	.run(function($ionicPlatform, $rootScope, $location) {

		//EDIT THESE LINES
		//Title of the blog
		$rootScope.TITLE = "Raymond Camden's Blog";
		//RSS url
		$rootScope.RSS = "http://www.raymondcamden.com/rss.cfm";

		$rootScope.goHome = function() {
			$location.path('/entries');
		};
		
	});

}());

Next - the controllers file. This is all pretty standard I think - nothing special. Again though I used a hack to detect when Ionic reloaded my app and I wasn't on the home page. Since the home page loads RSS entries, if they don't exist in the root scope, I just relocate back home. This worked for my app, it may not work for yours.

(function() {
'use strict';
/* global window,angular,console,cordova,google */

	angular.module('rssappControllers', [])

	.controller('HomeCtrl', ['$ionicPlatform', '$scope', '$rootScope', '$cordovaNetwork', '$ionicLoading', '$location', function($ionicPlatform, $scope, $rootScope, $cordovaNetwork, $ionicLoading, $location) {
		
		$ionicLoading.show({
      		template: 'Loading...'
		});
		
		function initialize() {
			console.log('googles init called');	
			var feed = new google.feeds.Feed($rootScope.RSS);
			
			feed.setNumEntries(10);
			feed.load(function(result) {
				$ionicLoading.hide();
				if(!result.error) {
					$rootScope.entries = result.feed.entries;
					console.log('move');
					$location.path('/entries');
				} else {
					console.log("Error - "+result.error.message);
					//write error
				}
			});

		}
		
		$ionicPlatform.ready(function() {

			console.log("Started up!!");

			if(window.cordova && window.cordova.plugins.Keyboard) {
				cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
			}
			if(window.StatusBar) {
				window.StatusBar.styleDefault();
			}

			if($cordovaNetwork.isOnline()) {
				google.load("feeds", "1",{callback:initialize});
			} else {
				console.log("offline, push to error");
				$ionicLoading.hide();
				$location.path('/offline');

			}

		});

	}])

	.controller('EntriesCtrl', ['$scope', '$rootScope', '$location', function($scope, $rootScope, $location) { 
		console.log('EntriesCtrl called');
		/*
		handle issue with Ionic CLI reloading
		*/
		if(!$rootScope.entries) $location.path('/');
		
		$rootScope.notHome = false;
		
		$scope.entries = $rootScope.entries;
		console.log(JSON.stringify($scope.entries[0]));

	}])
	
	.controller('EntryCtrl', ['$scope', '$rootScope', '$location', '$stateParams', function($scope, $rootScope, $location, $stateParams) { 
		console.log('EntryCtrl called');

		/*
		handle issue with Ionic CLI reloading
		*/
		if(!$rootScope.entries) $location.path('/');

		$rootScope.notHome = true;
		
		$scope.index = $stateParams.index;
		$scope.entry = $rootScope.entries[$scope.index];
		
		$scope.readEntry = function(e) {
			window.open(e.link, "_blank");
		};
		
	}]);

	
}());

So that's pretty much it. I have a few templates, but only the entry list is that interesting. Here it is.

<ion-view title="{{TITLE}}">

	<ion-content class="padding">

		<ion-list class="list list-inset">

			<ion-item ng-repeat="entry in entries" href="#/entry/{{$index}}">
				{{entry.title}}
			</ion-item>

		</ion-list>

	</ion-content>
	
</ion-view>

All in all - it was cool to rewrite this. I still wish there was an easier solution for the DeviceReady issue (I swear it seems like I'm the only person annoyed by this) and the reloading, but in general, I really like this version better. I've still got respect for jQuery Mobile (and, ahem, sell a good book on it ;), but Angular just feels much better for my Cordova apps, and Ionic's UI feels easier to use as well.

You can find the complete code under my main Cordova Examples repo, https://github.com/cfjedimaster/Cordova-Examples/tree/master/rssreader_ionic.