If you have been following the Diving Into
The app that we develop is going to highlight a few features of Kendo UI Mobile and utilize some Cordova plugins built into
Every great mobile app needs a great idea behind it, and what better way to demonstrate the power of
Equally as important as defining what is in scope, we should also take a moment to discuss what is out of scope for this project. Due to time constraints we are going to exclude the Reddit staples of user authentication, voting, and comments. I know, what is the use of building this app without being able to vote up a cat meme?
When we build this app we are essentially building a Single Page Application (SPA).
Now that we know what we are building and what tools we have at our disposal,
All of the code presented here is available on GitHub at https://github.com/rdlauer/icenium-reddit.
Create a new project in
If you want to skip copying and pasting all of this code, you could choose "Clone" instead and enter the URL to the GitHub repository in the "Repository Uri" field. This will pull in a copy of the most recent version and you can follow along that way.
Remove everything that is in your default index.html document and paste in the following code. This is our boilerplate HTML5 Cordova app template with a couple of commented-out references to our ChildBrowser and SQLite plugins (more on those later).
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta charset="utf-8" />
<script src="cordova.js"></script>
<!--
<script src="Plugins/Child Browser/childbrowser.js"></script>
<script src="Plugins/SQLite/SQLitePlugin.js"></script>
-->
<script src="kendo/js/jquery.min.js"></script>
<script src="kendo/js/kendo.mobile.min.js"></script>
<script src="scripts/db.js"></script>
<script src="scripts/app.js"></script>
<link href="kendo/styles/kendo.mobile.all.min.css" rel="stylesheet" />
<link href="styles/main.css" rel="stylesheet" />
</head>
<body>
<script>
var app = new kendo.mobile.Application(document.body, { transition: "slide" });
</script>
</body>
</html>
Everything else should make at least some sense to you. At the
Our home view is where our users will see all of their
<!-- home view (list of subreddits) -->
<div data-role="view" id="home" data-init="getSubReddits">
<header data-role="header">
<div data-role="navbar">
<a href="#subreddit-add" data-role="button" data-icon="add" data-align="left" data-rel="actionsheet"></a>
<span data-role="view-title">reddit on icenium</span>
<a data-role="button" data-click="toggleDeleteButtons" data-align="right" class="toggleButton">Edit</a>
</div>
</header>
<ul id="subreddit-list"></ul>
<ul data-role="actionsheet" id="subreddit-add">
<input type="text" placeholder="Subreddit Name" id="txtNew" />
<li><a href="#" data-action="addSubReddit">Add Subreddit</a></li>
</ul>
</div>
Our home view is just a div element with a data-role attribute = view. This is how we declare it as a Kendo UI Mobile view. In addition, there is a data-init attribute = getSubReddits. This means when our view is first initialized (first viewed), we are going to execute the "getSubReddits" JavaScript function.
The header element has a div with a data-role of navbar. This is the navigation bar of our app. Inside of our navigation
Underneath our header
<!-- home view (kendo template for subreddit list) -->
<script type="text/x-kendo-template" id="subreddit-list-template">
<a href="\#subreddit?name=${name}">${name}</a>
<a data-role="detailbutton" data-style="rowdelete" data-click="removeSubReddit" data-id="${id}" class="delete"></a>
</script>
When we get to
We're almost done with our HTML! As you can probably tell we are trying to keep our HTML as simple as possible and leave the heavy lifting to the Kendo UI framework.
We have one more view and one more template to add. These will allow us to view individual
<!-- subreddit view (list of top 25 subreddit submissions) -->
<div data-role="view" id="subreddit" data-show="getSubRedditData">
<header data-role="header">
<div data-role="navbar" id="subreddit-data-navbar">
<a data-align="left" data-role="backbutton">Home</a>
<span id="view-title" data-role="view-title"></span>
</div>
</header>
<ul id="subreddit-data"></ul>
</div>
<!-- subreddit view (kendo template for top 25 subreddit submissions) -->
<script type="text/x-kendo-template" id="subreddit-data-template">
<div data-role="touch" data-tap="openExternalURL" data-url="${data.url}">
<div style="float:left">
<img src="${data.thumbnail}" class="thumb" />
</div>
<div class="reddit-content">
<span class="reddit-content-title">${data.title}</span>
<br />
<span class="reddit-content-domain">${data.domain}</span>
<br />
# var points = data.ups - data.downs #
<span class="reddit-content-points">${points} in ${data.subreddit} by ${data.author}</span>
</div>
</div>
</script>
Take a moment to look over this code. You'll see in the header we are adding a back button with data-role="
And that's it for your HTML!
In the interest of preserving time (and assuming that you already know enough CSS to gloss over this part) I'm only going to provide you a link to the main.css file that will go in your styles directory: get the CSS here.
Now that we have our HTML and CSS,
Our
Copy all of this code and paste it as db.js in your scripts directory:
var sqlite = function () {
var defaultData = {
subreddits: [
{"name": "reddit Front Page"},
{"name": "pics"},
{"name": "funny"},
{"name": "gaming"},
{"name": "videos"},
{"name": "IAmA"},
{"name": "todayilearned"},
{"name": "AdviceAnimals"},
{"name": "movies"},
{"name": "AskReddit"}
]
};
// initialize and create our default table
var init = function() {
if (window.sqlitePlugin !== undefined) {
sqlite.db = window.sqlitePlugin.openDatabase("reddit");
}
else {
// For debugging in simulator fallback to native SQL Lite
//console.log("Using built-in SQL Lite");
sqlite.db = window.openDatabase("reddit", "1.0", "reddit on icenium demo", 200000);
}
sqlite.db.transaction(function(tx) {
// create our table if it doesn't already exist
tx.executeSql("CREATE TABLE IF NOT EXISTS subreddit (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)", []);
// check to see if we already have records in our table - it not, populate with default values
tx.executeSql("SELECT * FROM subreddit", [],
checkSubRedditCount,
logError);
});
}
var checkSubRedditCount = function (tx, rs) {
var self = this;
if (rs.rows.length == 0) {
$.each(defaultData.subreddits, function(index, data) {
self.sqlite.insertRecord(data.name);
});
}
getSubReddits(); // load our home view with our newly inserted list of subreddits
}
var logSuccess = function(tx) {
console.log("SQLite Query Executed: " + tx);
}
var logError = function(tx, e) {
console.log("SQLite Error: " + e);
}
var insertRecord = function(name) {
sqlite.db.transaction(function(tx) {
tx.executeSql("INSERT INTO subreddit (name) VALUES (?)", [name],
logSuccess,
logError);
});
}
var deleteRecord = function(id) {
sqlite.db.transaction(function(tx) {
tx.executeSql("DELETE FROM subreddit WHERE ID=?", [id],
logSuccess,
logError);
});
}
var selectAllSubReddits = function(fn) {
sqlite.db.transaction(function(tx) {
tx.executeSql("SELECT * FROM subreddit ORDER BY id", [],
fn,
logError);
});
}
return {
init: init,
logSuccess: logSuccess,
logError : logError,
insertRecord: insertRecord,
deleteRecord: deleteRecord,
selectAllSubReddits: selectAllSubReddits
}
}();
In this file you will see examples of inserting, selecting, and deleting records with SQLite. In addition you can see how we initialize our SQLite database and create a database table using a default set of data.
At this point our app still can't "do" anything. We have our UI and our data access layer complete, but we haven't brought everything together. This all happens in our app.js file. Create this as app.js in your scripts directory:
// Wait for PhoneGap to load
document.addEventListener("deviceready", onDeviceReady, false);
// PhoneGap is ready
function onDeviceReady() {
navigator.splashscreen.hide();
sqlite.init(); // initialize the database
}
// get a list of subreddits for our home view from our sqlite database
function getSubReddits() {
var render = function (tx, rs) {
var d = $.parseJSON(convertRStoJSON(rs));
$("#subreddit-list").kendoMobileListView({
dataSource: d,
template: $("#subreddit-list-template").html(),
style: "inset"
});
}
if (sqlite.db) {
sqlite.selectAllSubReddits(render);
}
}
// get the top 25 submissions for the specified subreddit
function getSubRedditData(e) {
var name = e.view.params.name;
$("#subreddit-data-navbar").data("kendoMobileNavBar").title(name); // set the title in the nav bar
if (name == "reddit Front Page") {
name = "";
} else {
name = "r/" + name + "/";
}
$.getJSON("http://www.reddit.com/" + name + ".json?jsonp=?", function(data) {
$("#subreddit-data").kendoMobileListView({
dataSource: data.data.children,
template: $("#subreddit-data-template").html(),
style: "inset"
});
});
}
function toggleDeleteButtons() {
if ($(".toggleButton").text() == "Edit") {
$(".delete").show();
$(".toggleButton").text("Done");
} else {
$(".delete").hide();
$(".toggleButton").text("Edit");
}
}
function openExternalURL(e) {
var url = e.touch.target[0].attributes[2].nodeValue;
if (window.plugins && window.plugins.childBrowser) {
window.plugins.childBrowser.showWebPage(url);
} else {
console.log(url);
}
}
function addSubReddit() {
if ($("#txtNew").val().length == 0) return;
sqlite.insertRecord($("#txtNew").val());
$("#txtNew").val("");
// refresh our listview with the new data
var listView = $("#subreddit-list").data("kendoMobileListView");
listView.refresh();
}
function removeSubReddit(e) {
var data = e.button.data();
sqlite.deleteRecord(data.id);
$("a[data-id='" + data.id + "']").closest("li").remove();
}
function convertRStoJSON(rs) {
var arr = [];
for (var i = 0; i < rs.rows.length; i++) {
arr.push(rs.rows.item(i));
}
return JSON.stringify(arr);
}
Lets go over some of the highlights of app.js to avoid any confusion:
onDeviceReady is executed when Cordova is initialized, informing us that we now have access to Cordova functions. This is where we will initialize our SQLite database.
getSubReddits will execute a database query to select all of the subreddits from our SQLite table. In turn we are converting the recordset to JSON and binding that to our Kendo Mobile ListView ("subreddit-list" in our home view).
getSubRedditData is passed the name of our subreddit, sets the title of our view to the subreddit name, makes a call to Reddit, and displays the top 25 posts in the selected subreddit.
openExternalURL is how we use the ChildBrowser plugin to display a web browser in our app context. Notice that we are checking if window.plugins returns true. The Graphite Simulator cannot execute Cordova plugins, so we have to work around them in our code. In this case we are outputting the URL to the console for testing purposes.
addSubReddit and removeSubReddit are pretty self-explanatory. These support the addition and removal of subreddits from our database table.
Finally convertRStoJSON is a function used to convert our recordset to JSON format since the Kendo UI DataSource does not natively support SQLite recordsets.
Now we need to add the ChildBrowser and SQLite plugins to our app. Open up your app Properties (either right-click on your project name in the Project Navigator or double-click on the Properties item). Choose the Plugins menu option and check the boxes next to ChildBrowser and SQLite. Save your properties and you should then see a new "Plugins" directory show up in your Project Navigator.
We already added references to the two JavaScript files that support these plugins, but left them commented out. Why you ask? The Graphite Simulator does not support Cordova plugins, so we need to leave those references out and account for the lack of support in our code (which we have already done). When you are ready to test on a mobile device, simply uncomment those two lines and test away.
Assuming we have done everything correctly, you should now have a fully functional mobile app that we can test with the Graphite Simulator or your mobile devices. Remember that since we are using Kendo UI Mobile, the UI of our app will automatically change based on the device it is displayed on. So if we are using an iOS device you will see the iOS look and feel and if you are on Android you will see the Android look and feel (same goes for Windows Phone 8 and Blackberry).
During real development you will, of course, be testing throughout the entire process. In our case we'll test our completed app using the available Icenium testing options: the Graphite Device Simulator and physical mobile devices. You will most likely use the Graphite Device Simulator during the early phases of development. It provides you with the ability to quickly test your UI and functionality on both iOS and Android devices and OS versions. However, we all know that simulators can only do so much - and your final testing should always be done on the devices themselves. Lets quickly go over both types of testing now:
You may initiate the Graphite Simulator at any time by pressing F5 or going to the Run -> In Simulator main menu option. By default you will see an iPhone 4/4S device running your app in iOS 6. You can switch to a different device and/or a different OS version by using the appropriate menu options at the top of your screen.
You also have the ability to see how your app reacts when the device is flipped over 180 degrees or turned by 90 degrees in either direction.
The simulator also has Refresh and Reload buttons which allow you to refresh your current view or reload the entire app. This leads us to the next great feature of Icenium Graphite, LiveSync. LiveSync allows you to see changes made to your app in real-time without having to rebuild and deploy the project. This includes changes not only to your UI (HTML/CSS) but also any JavaScript modifications. Make a change to any asset in your project and you will see the change reflected almost instantly.
The simulator also has a full-featured Debug menu that allows you to access the developer tools, which we will discuss later on when we dive into debugging.
Icenium Ion is a convenient way to test your app on an iOS device without going through the provisioning steps otherwise required with a physically-connected device.
Once your app is running on your device you can use it just like you would use any other app. Make a change to the code? Simply hold three fingers down on the app and it will automatically go out and download the latest version available. How simple is that!?
Another way to test your app is to physically connect an iOS or Android device to your computer. Simply connect your device and, provided you have the correct drivers installed, you can choose a device and start testing your app. Please note that if you are testing on an iOS device, you must have a provisioning profile set up for the device. We will cover this area in the next post in this series.
While you are using the Graphite Simulator, you have access to some very powerful debugging tools. Choose the Debug menu option in the simulator and your developer tools will pop up at the bottom of your screen. If this looks familiar to you, it is because Graphite is using the same developer tools you use when you debug with the Chrome web browser!
You have the ability to inspect your source code using the Elements panel as if it were a standard web site. The Resources panel allows you to view any databases you have created or to inspect Local Storage. Network is where you can view downloaded resources to see if anything there is impacting your app performance. Scripts allows you to debug your JavaScript. Console is a key to diagnosing problems in your app.
There is plenty more to see and do with the debugging tools. For a more in-depth look I suggest reading up on the Chrome Developer Tools.
We did it. We built a hybrid mobile app with Icenium and Kendo UI Mobile. We tested it using the built-in Graphite Simulator and on our mobile devices. Finally, we learned a little about the debugging options available in the simulator. What is next? Our last post in this series will look into the murky waters of provisioning/code signing, take a closer look at the built-in version control options, and learn about publishing your app to the Apple and Google Play app stores!
A maker at heart and a supporter of the open web, Rob is Developer Relations Lead at Blues Wireless. You can find Rob rambling incoherently on Twitter @RobLauer.