Amber Alert: We can do mobile apps!

How to make cross-platform apps using Amber


As an Ambersmith, you may be primarily interested in writing client-side, browser-based applications similar to what you can do with AngularJS, Ember, or Backbone. However, I am particularly interested in writing cross-platform mobile apps because I don’t like writing four substantially different applications for Android, iOS, Windows, and BlackBerry. And in the future, there may be additional platforms such as Firefox, Tizen, and Ubuntu.

Enter Cordova, the platform for building native mobile applications using only HTML, CSS, and JavaScript. After you install Cordova with the command line [1]:

npm install -g cordova

You create your Cordova application in the projects folder of your choice with command line:

cordova create YourAppFolder com.YourNameOrCompany.YourApp "Your App Title"

CD to YourAppFolder and add a device platform. My first platform will be Android:

cordova platform add android

Add some basic plugins to your project:

cordova plugin add cordova-plugin-device
cordova plugin add cordova-plugin-media
cordova plugin add cordova-plugin-geolocation

An Amber application must live within the Cordova program structure, i.e., it replaces the existing contents of the ‘www’ folder of the Cordova application. This is because Cordova (cordova.js) is not a proper JavaScript library that can be imported into Amber; it has a great many dependencies on other parts of Cordova. So after you’ve created your Cordova application, erase the ‘www’ contents, CD into ‘www’ and run ‘amber init’.

I created two different index.html files, one for development and one for deployment on the device/emulator. The latter deletes the dialog line (“amber-ide-starter-dialog”) for starting Helios, because you can’t run it in Android, and runs the Amber startup code within the onDeviceReady() function.

document.addEventListener("deviceready", onDeviceReady, false);
function onDeviceReady() {
require(['app'], function (amber) {
amber.initialize({
//used for all new packages in IDE
'transport.defaultAmdNamespace': "amber-yourapp"
});
amber.globals.YourApp._start();
});
}

Add this meta tag to the header of your deployment index.html:

<meta http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'">

This will let you run your JS scripts in the device without incurring CSP warnings in the JS console.

In both files, you add the following line just before the JS script tag that starts up Amber:

<script src="cordova.js"></script>

Don’t worry, Cordova knows how to include cordova.js at build time.

On the Mac, I also created a couple of scripts (in the ‘www’ folder) for toggling between development in the desktop browser and deployment on the device (or emulator, if device is not available):

# sh run-dep
if [ -e index-dep.html ];
then
mv index.html index-dev.html
mv index-dep.html index.html
grunt deploy
fi
cordova run android
# sh run-dev
if [ -e index-dev.html ];
then
mv index.html index-dep.html
mv index-dev.html index.html
grunt devel
fi
amber serve

In development, you run ‘amber serve’ and develop in the browser with Helios. Of course, any device-specific behaviour will not work here. It’s mainly for writing business logic.

Most of your app can be written in Amber (along with CSS and HTML). Less than 10% will require direct JavaScript coding, and this will be mostly for device access, such as battery status, camera, geolocation, media, etc.

The Android emulator is glacially slow and very unreliable; I would not recommend using it. You should use an actual Android device for testing and debugging. On my OnePlus One smartphone, I need to enable “USB debugging.” However, this option isn’t normally available to the user. In order to enable USB debugging, you must first go into Settings->About phone, tap on “Build number” 7 (seven) times, and then “Developer options” will be enabled in the Settings screen. From there, you can choose “Android debugging” and you’re good to go.

(UPDATE April, 2017: the latest Android emulator is not so slow anymore. It actually runs acceptably well. Thank you, Google!)

Debugging Your Cordova Application

In deployment on your device, you want to test your app and investigate any issues. I use the Chrome browser and ‘chrome://inspect’ to debug problems. As a long-time fan of the trusty, ol’ “print statement” for debugging software, I’m telling you that console.log() is your friend! (In the emulator/simulator, alert() is your friend.)

You can also use GapDebug.

Background Images

I want to display a background image in the app. The following in a CSS file puts a “splash” image in the background:

body {
background-image: url("lotus_flower.png");
background-position: center top;
}

But I also want to optionally display other images:

showImage
'#picture-frame' asJQuery append:
'<style>#picture-frame {background-image:
url(',(images at: cursor),');
position: fixed;
top: 0; left: 0; bottom: 0; right: 0;
background-size: cover;
background-position: center;
}</style>'

This automagically handles orientation changes and shows the image filling the screen.

Playing Audio on the Device

I’ll show you how to access one of the device’s hardware capabilities: playing audio. After you’ve added the Media plugin, as shown above, your Cordova application can instantiate the Media object for playing music and videos.

Unfortunately, you cannot access Cordova’s plugin “classes” from Amber; as I mentioned earlier, Cordova cannot be imported into Amber. However, there is a workaround. In index.html for deployment, I wrote this script:

document.addEventListener("deviceready", onDeviceReady, false);
var media;
// this function replays the audio after it has finished/stopped
var loop = function (status) {
if (status === Media.MEDIA_STOPPED) {
media.play();
}
}
// event triggered when app goes into the background
document.addEventListener("pause", onPause, false);
function onPause() {
media.pause();
}
// event triggered when app resumes in the foreground
document.addEventListener("resume", onResume, false);
function onResume() {
media.play();
}
// this is Amber's "interface" to the Media object
function playNewTrack(track) {
media.stop();
media.release(); // IMPORTANT: must release the audio resources
media.src = track;
media.play();
}
// return the correct prefix string for Android
function prefix() {
if (device.platform == "Android") return "/android_asset/www/";
else return "";
}
function onDeviceReady() {
// start the Amber application when ready
Require(['app'], function (amber) {
amber.initialize({
//used for all new packages in IDE
'transport.defaultAmdNamespace': "amber-yourapp"
});
amber.globals.YourApp._start();
});
    // Initialize the Media object, which can be reused in
// playNewTrack() from Amber.
// By default, we always loop the audio.
media = new Media("/android_asset/www/", null, null, loop);
}

The function playNewTrack() can be executed from Amber, for example:

mySound := Audio new. "only create one instance, ever!"
aTrack := 'assets/audio/Ben_Dowling.mp3'.
"If the media object is nil, then we aren't in the device, so
we fall back on the browser's Audio."
window media ifNil: [
mySound loop: true.
mySound src: aTrack.
mySound play]
ifNotNil: [window playNewTrack: (window prefix),aTrack]

Note the prefix ‘/android_asset/www/’ — it is required for Android apps to access their internal resources. As my app allows the user to switch audio tracks repeatedly, I insist on having only one instance of the media object in order to avoid memory issues (JavaScript’s garbage collector runs unpredictably). Execute ‘Audio new’ in an appropriate place [2].

Note that the Media plugin is not W3C-compliant. Expect it to be deprecated in the future once a W3C-compliant solution is released.

Using Swipe to Change Audio Track

We would like to use a touch swipe gesture to switch audio tracks. For simplicity, I’ve adopted a pure JavaScript library from PADILICIOUS. With this, I can sense an up or down swipe and use this to trigger playback for the next audio track. Just save the file as swipesense.js without the <script> tag. (And change the misspelt function name from ‘caluculateAngle’ to ‘calculateAngle’.) Then, in deployment index.html, add this line:

<script src="swipesense.js"></script>

Also, add this DOM element:

<div id="picture-frame" ontouchstart="touchStart(event,'picture-frame');" ontouchmove="touchMove(event);" ontouchcancel="touchCancel(event);"></div>

With this styling:

#picture-frame {
/* background: silver; for testing purposes */
position: fixed;
top: 0; left: 0; bottom: 0; right: 0;
background-size: cover;
background-position: center;
}

In swipesense.js, I need to modify touchEnd() to allow my own swipe processing in Amber:

function touchEnd(event, myProcessing) {

if ( swipeLength >= minLength ) {
calculateAngle();
determineSwipeDirection();
//processingRoutine();
myProcessing();

touchCancel(event); // reset the variables
} else {
touchCancel(event);
}

And in Amber:

myElement := '#picture-frame' asJQuery at: 0.
myElement addEventListener: 'touchend' func: [:ev |
window touchEnd: ev myProcessing: [self processSwipe]].

Where ‘processSwipe’ is:

processSwipe
| direction |
direction := window swipeDirection.
(direction = 'up' or: direction = 'down') ifTrue: [
self doNextTrack].
direction = 'left' ifTrue: [
self doPrevImg].
direction = 'right' ifTrue: [
self doNextImg].

Finding Where We Are

I’ll show you how to access another one of the device’s hardware capabilities: geolocation. After you’ve added the geolocation plugin, as shown above, your Cordova application can determine your geographical coordinates (among other information) via GPS and/or Wi-Fi and mobile network.

In Amber, you can get your current position thus:

navigator geolocation getCurrentPosition: [:position |
alert value: 'Lat: ',(position coords latitude),
(String cr),'Long: ',(position coords longitude),
(String cr),'Altitude: ',(position coords altitude),
(String cr),'Heading: ',(position coords heading),
(String cr),'Speed: ',(position coords speed),
(String cr),'Time: ',(position timestamp)]
onError: [:error |
alert value: 'code: ',(error code),(String cr),
'message: ',(error message)]

Yes, it’s that simple!

Using the iOS Simulator

I don’t have an iPhone, so I’ll show you how to get the above app running in an iOS Simulator. The iOS Simulator is pretty slow, too, but at least it’s faster than the Android emulator, and a lot more stable to boot.

You’ll need to add iOS support (make sure you’ve installed Xcode 6.x.):

cordova platform add ios

You can modify the Android script for iOS:

# sh run-ios
if [ -e index-dep.html ];
then
mv index.html index-dev.html
mv index-dep.html index.html
grunt deploy
fi
cordova emulate ios

And everything works as before, only in a simulator! Use alert() for debugging.

For geolocation, make sure you go into Debug->Location and choose anything but “Custom Location…”, say, “Apple.” Otherwise, geolocation will fail.

Note – there is one area where iOS falls flat on its face: portrait/landscape detection. At least in the simulator, and probably in the hardware too, turning the phone to landscape does not re-orient the application. There are workarounds available apparently, but they are beyond the scope of this tutorial.

Conclusion

Hopefully, this will help you get started in writing your cross-platform mobile app. It’s really nice to have a powerful and easy-to-use tool like Amber to help improve your productivity and avoid the ugliness of JavaScript. Amber is fun, easy to learn, easy to read, easy to master. Just plain easy!


[1] This assumes you’ve installed Node.js, as per my previous Amber tutorials. Also, make sure you’ve installed Java Runtime Environment (JRE) 6 and Java Development Kit (JDK) 7, if you’re writing for Android and/or Xcode 6.x, if you’re writing for iOS.

[2] Safari really doesn’t like ‘Audio new’–the Audio class isn’t supported.