Time.zipDeveloping an Android-iOS, smartphone-tablet app in one
month
Ivano Malavolta and Alessio d'Arielli
Ivano MalavoltaResearch fellow @University of L'Aquila
Alessio d'ArielliGraphic Designer and Web developer
RoadmapApp overview
Organizational best practices
Technical best practices
AppOverview
The Customer
Who is Frascati ScienzaWhat kind of app they asked for
Stuff provided
A Storyboard (with many revisions)
Stuff provided
Photoshop Mockups (with many revisions too)
Stuff provided
Anxiety (with many emails and phone calls)
Technical constraintsPlatforms to support: Android 4.x & iOS 6.xDevices: Smartphone & tabletOffline supportQR code scannerVideo streamingIn-app browserNews from RSS feeds
Timeline
2 people, ~4 hours a day
Total working hours: ~320
(~1 month, 2 people full time)
The app
More than 5000 participants to the event
The treasure hunt carried on in the center of Rome
It involved 1000 people, 150 participants
OrganizationalBest
Practices
1. Debug contract before the code
Ask about the operational flow, the UI / UX, the intention of
the app IMMEDIATELY. Write the whole thing in the contract.
Why? If you rework parts already completed you arespending money from your own pocket
2. Web-based feasibility
If they ask to support multiple platforms, evaluate if web-
based development (PhoneGap) is feasible
Search and test any component for advanced features(in our case, QR-code scanning and child browser)
3. Balance efforts
Equally divide the workload, responsibilities and testing.Notify quickly to colleagues about bugs.
4. Use code repositories
Use repository for remote collaboration (SVN, git, etc.)rather than email or other methods that have no chance to
undo.
5. Avoid micro-multitasking
Focus on and complete a task at time, otherwise probability
of injecting bugs is terribly high.Optimize your schedules.
6. Use MVC pattern
By structuring the app with the MVC pattern, you can workin parallel on JS and frontend HTML5/CSS3
7. Modularity
Max simplification of navigation and extreme modularity soany changes to the content or presentation are quickly
realizable without affecting the overall structure.
8. Be reactive
Maximise reactivity and cooperation with colleagues.
Don't empower customer's anxietyresponding immediately to every request
9. Organize revisions with customers
Keep the customer in the loop, arrange periodical meetingsin order to update him and to collect a list of tasks or
corrections to be made.
10. Get all testing tools
Book many hours for testing (and blasphemy),
get as soon as you can as much devices as possible fortesting.
Summary1. Debug contract before the code
2. Web-based feasibility
3. Balance efforts
4. Use code repositories
5. Avoid micro-multitasking
6. Use MVC pattern
7. Modularity
8. Be reactive
9. Organize revisions with customers
10. Get all testing tools
TechnicalBest
Practices
1. First day: search and test alreadydeveloped components
require.config({ paths: { jquery: '../lib/jquery/jquery-1.9.1.min', /* '../lib/jquery/zepto', */ underscore: '../lib/underscore/underscore-min', backbone: "../lib/backbone/backbone", text: '../lib/require/text-1.0.6', async: '../lib/require/async', handlebars: '../lib/handlebars/handlebars', templates: '../templates', leaflet: '../lib/leaflet/leaflet', datamanager: 'datamanager', spin: '../lib/spin/spin' }, shim: { 'jquery': { exports: '$' }, 'underscore': { exports: '_' }, 'backbone': { deps: ['jquery', 'underscore'], exports: 'Backbone' }, 'handlebars': { exports: 'Handlebars' }, 'leaflet': { exports: 'L' } }});
Once you are done, setup your boilerplate app
2. Establish the software architecturefrom the beginning
It will guide the structure of your code
var EnteView = Backbone.View.extend({ model: Ente,
initialize: function () { this.title = this.model.get("titolo");
},
var EventoView = Backbone.View.extend({ model: Evento, /**/ initialize: function () { var date = new Date(this.model.get("timestamp") * 1000); var gg, mm, aaaa, hours, mins; gg = date.getDate() + "/"; mm = date.getMonth() + 1 + "/"; aaaa = date.getFullYear(); hours = date.getHours(); mins = date.getMinutes(); this.title = gg + mm + aaaa + " - " + hours + ":" + mins; /**/ },
Define your own coding patterns**more patterns will show up during the presentation
3. Clearly separate concerns
It makes the code more testable
More easy to extend and refine the app
It allowed us to follow a micro-process
from JS developer's perspective...
I commit this after update I see this
Example (home view)
define([..., "text!templates/frascatiscienza.html"], /* dependency to template file */function ($, _, Backbone, Handlebars, Ente, template) { /* template contains a string now */ var FrascatiScienzaView = Backbone.View.extend({
model: Ente,
className: "default_wrapper",
events: { "touchstart #enti": "enti", "touchstart #_eventi": "eventi", "touchstart #partner": "partner", "touchstart #frascati": "continua" },
template: Handlebars.compile(template), /* template compilation, it is a function now */
render: function () { /* gestione nav bar */ this.updateNavbar(); $(this.el).html(this.template(this.model.toJSON())); /* template execution */ /* */ return this; } }); return FrascatiScienzaView;});
4. Keep performance in mind from thebeginning
Avoid to fall into the fancy-framework trapUse pure JS as much as you canNo JS animation, just switch classes + CSS3transitions/transformsUse native touch events, not onClick (300ms delay)Minimize browser reflowsAvoid complex CSS selectorsTry to use id-only selectors...
5. Views first, then datavar AppRouter = Backbone.Router.extend({ /* */changePage: function (page) { if (this.currentView) { this.currentView.trigger("removed"); this.currentView.remove(); } this.currentView = page; this.structureView.currentView = page; page.render(); this.structureView.$el.find("#content").append($(page.el)); this.structureView.trigger("updateTitle", page); this.currentView.trigger("inTheDom"); // here the new view can fetch data return true;}/* */
Valid especially when data is coming from the network
6. Let the user forget he is looking at abrowser
@charset "UTF-8";/* STANDARD FOR MOBILE */
* { /* transparent link selection */ -webkit-tap-highlight-color: rgba(0,0,0,0);}
body { -webkit-touch-callout: none; /* no callouts during tap and hold */ -webkit-text-size-adjust: none; /* no fonts auto inflation */ -webkit-user-select: none; /* no copy and paste, etc. */ background-attachment: fixed; font-family: 'HelveticaNeue-Light', 'HelveticaNeue', Helvetica, Arial, sans-serif; height: 100%; margin: 0px; padding: 0px; width: 100%;}
#mainContainer { /* get the whole display */ position: absolute; height: 100%; width: 100%; margin: 0px; padding: 0px; left: 0; top: 0;
7. Minimize network access// We launch the Apprequire(['underscore', 'backbone', 'spin', 'router', 'datamanager'], function (_, Backbone, Spinner, AppRouter, Data) {
String.prototype.endsWith = function (suffix) { return this.indexOf(suffix, this.length - suffix.length) !== -1; };
String.prototype.strip = function () { return this.replace(/(<([̂>]+)>)/ig, "").replace(/(<([̂>]+)>)/ig, ""); };
document.addEventListener("deviceready", run, false);
function run() { Data.initialize();
/* */ } }); Try to prefetch data as much as possible
If some data is always the same, bundle it into the app
8. Take special care of imagesAvoid to resize images (both via CSS and JS)Be robust w.r.t. 404 errors
Show a spinner in place of an image while it is loading
/* in the template */<img src="{{ immagine }}" onerror="ImgError(this);">
/* in your JS */function ImgError(source){ empty1x1png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQI12NgYAAAAAMAASDVlMcAAAAASUVORK5CYII=" source.src = "data:image/png;base64," + empty1x1png; source.onerror = ""; return true;}
.heavyImg { background: url('../res/loader.gif') no-repeat; background-position: center; min-height: 20%; width: 100%;}
9. When it's simple, leave it simpleDon't overelaborate. Complexity will come by itself.
goBack: function (self) { var that = (self instanceof StructureView) ? self : this; if (!that.currentView) { return false; } if (that.currentView instanceof IntroTappaView) { return false; } if (that.currentView instanceof DomandaCacciaView) { return false; } if (that.currentView instanceof RisultatoCacciaView) { Backbone.history.navigate("introcaccia", {trigger: true}); return false; } if (that.currentView instanceof FineCacciaView) { Backbone.history.navigate("caccia", {trigger: true}); return false; } window.history.back();}
ANTIPATTERN ABOVE
10. Test, debug, test, debug
In this context, your desktop browser is the killer app!
Check consoleBreakpointsUpdate the DOM at run-timeAccess to all local DBsNetwork profilingCPU and memory profilingMonitor event listenersMonitor elements’ rendering time
Summary1. First day: search and test already developed components
2. Establish the software architecture from the beginning
3. Clearly separate concerns
4. Keep performance in mind from the beginning
5. Views first, then data
6. Let the user forget he is looking at a browser
7. Minimize network access
8. Take special care of images
9. When it's simple, leave it simple
10. Test, debug, test, debug
ConclusionsWe extracted a set of organizational and technical bestpractices from a true story
In any case, step zero to success is to be technologicallyready
for example, many people tend to underestimate JavaScript, don't!
BonusAn RSS reader in 20 lines of pure JavaScript ;)
define(["jquery", "underscore", "backbone", "models/Rss"], function ($, _, Backbone, Rss) {
var RssList = Backbone.Collection.extend({
model: Rss,
populate: function (feedUrl, view) { var xmlhttp = new XMLHttpRequest(); var self = this; xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { var feed = xmlhttp.responseXML; var news = feed.getElementsByTagName("item"); var title, description, link; for (var i = 0; i news.length; i++) { title = news[i].getElementsByTagName("title")[0].textContent.strip description = news[i].getElementsByTagName("description")[0].textContent link = news[i].getElementsByTagName("link")[0].textContent.strip if (title && description && link) { self.create({ title: title, description: description, link: link }); }
Ivano MalavoltaResearch fellow
www.ivanomalavolta.com
@IMalavolta
github.com/iivanoo