Mobile app development A to Z

Creating a cross platform app in Titanium

Ani Sinanaj
Caffeina Developers

--

What it is

If you’re reading this article you probably know what Titanium is. If you don’t, To put it simply, Titanium is a framework which lets you develop a mobile app that will work on both Android and iOS (and Windows phone) reusing the same code or 90% of it.

The best part, the programming language is JavaScript and we all ❤ javascript.

Trimethyl is a set of tools and libraries that tries to lower that 10% gap between the two, other than making some APIs easier to use reducing implementation time.

Installation

Installing Titanium is easy, either install the IDE or execute the following code in your terminal

npm i -g titanium \
alloy \
tishadow \
tisdk \
tn \
gittio

Installing Trimethyl is as easy

npm i -g trimethyl

This is possible because Trimethyl or as we like to call it, T, has a powerful cli.

Remember that if you install the Appcelerator IDE, you still have to install a few extra tools like below

npm i -g tishadow tn

Creating a new project

So let’s start with creating the new project. I recommend doing some things from the command line because it’s faster.

So first thing to do, open Terminal and move to the desired directory where you want to save the source code of your application.

My workspace will be ~/projects/trimethyl_example

mkdir -p ~/projects/trimethyl_example
cd ~/projects/trimethyl_example

Now I’m going to create a Titanium project. This one is simple, just follow the prompts in the cli.

If you’re using the IDE just create a new project and select Alloy. Obviously skip the next step and jump to trimethyl

ti create
Titanium CLI prompt to create a new project from scratch

The prompt is really simple, it asks for the type of project, which in this case is “1”, the platforms I want the app for, and “all” works fine for this example, app id and project name, you can leave the URL blank, it’s not required, and since I’m already in the working directory we can just type “.” .

This would be enough for me to start developing the app with what I’ve got this far. For those of you that have used Titanium since the early days, you would be ready to go. But since MVC is a pretty efficient programming pattern I’m going to add it by installing Alloy

Third step is adding Alloy

Note that ti create has created a folder with the “project name” inside our directory. So for simplicity I’ll cd into that new directory.

alloy new .

That was simple enough right? Now you could already build the app and it would run perfectly.

But the article isn’t about creating a simple Titanium project. It’s about making it even simpler with the use of Trimethyl, so now I’ll add the last piece.

trimethyl install

I’m going to select here some basic functionalities I think I’m going to use in my app.

I think I need the following

  • Bootstrap
  • App
  • Auth because I want to show you how easy it is to manage authentication, with Facebook so I’ll also add Auth.Facebook
  • Flow
  • Router
  • GA which obviously is google analytics,
  • HTTP
  • Logger I’ll explain this one below
  • Notifications
  • UIUtil I’m not sure I’ll use it but I’ll add it anyway
  • WebAlloy

I’ll also add some other utilities like Q Underscore.String REST Alloy Adapter and that’s it for now.

Select libraries using spacebar

When you press enter, the cli will ask you if you want to include some titanium modules needed for the libraries to work such as ti.safaridialog

For each titanium module you can answer A to add it in the tiapp.xml file, i to install it through gittio, s to skip installation, e to exit the prompt and h for the help

Installing titanium modules needed for Trimtheyl libraries. Not all are required
This is the final output

Logger works like Ti.API in the sense that I can call Logger.warn() or Logger.info() the same way I use Ti.API.warn() the problem with the latter though, is that if you try to dump a Backbone model in it, it’ll work in development (TiShadow) but it will crash the app in production without giving any reason for it. So we wrapped Ti.API with Logger adding some checks to make sure that if a log remains in production, it doesn’t kill the app.

We’re done with the creation of the project

Let’s build something

Before proceeding with the development, I’ll launch the app on the simulator to see what I’ve got so far. (shouldn’t be a lot)

To help with future testing, I’ll create a file named tn.json and add the following to it

{
"sim": ["build", "-T", "simulator", "-p", "ios", "--device-id", "SIMULATOR_ID"]
}
Cross platform app with Titanium & Alloy

Change SIMULATOR_ID with the id of the simulator you want to run the app in. In my case it’s an iPhone SE running iOS 10.3.1. To get the simulators id, run instruments -s devices this will list all available iOS simulators

Then to run it, type tn sim

As this article suggests, Sublime Text is one of the best text editors in terms of memory management, so I’ll fire it up to start coding.

The entry point for alloy apps is alloy.js which is used to set global variables and then alloy automatically launches the index defined by the controller index.js and the view index.xml

Need an idea before actually coding

So I’m thinking of creating an app that requires the user to login with Facebook and once logged in, I want to show them a list of data from the internet and obviously after that, when the user taps on a list item, I’ll show the single item page. I think I’m going to use the IMDB API and create something based on that. And I’ll just enable notifications so I can just send random notifications to people.

I’ll create a proxy for the IMDB API so I can cache the response and not make too many requests to the IMDB server. I’ll use the proxy server to register my users’ device id so I can send them notifications in the future.

Assume my IMDB proxy will expose the following API endpoints then

  • /movies
  • /movies/id
  • /notifications/subscribe
  • /notifications/unsubscribe
  • /notifications/activate
  • /notifications/deactivate

The Trimethyl notifications library uses 4 endpoints by default. The difference between subscribe and activate is that with the activate, the user remains subscribed to the server but the server won’t send any notifications to the device.

Setting up the project to use my Trimethyl libraries

What I start with at this point is opening up the project with sublime text $ subl . and then I open alloy.js. To use the subl tool provided by Sublime Text check out this stack overflow thread.

I’ll add a global function T to help me with requiring Trimethyl libraries.

var T = function(name) { return require('T/' + name); };
T('trimethyl'); // Requiring base trimethyl

It is very clear what it does as you can see. It includes in the project all the Libraries I need by doing the following.

var Auth = null;
var HTTP = null;
var App = T('app');
var Event = T('event');
var Flow = T('flow');
var Logger = T('logger');
var Notifications = T('notifications');
var Q = T('ext/q');
var Router = T('router');
var Util = T('util');
var UIUtil = T('uiutil');
var WebAlloy = T('weballoy');
var GA = T('ga');

WebAlloy replicates Alloy MVC but for webviews. This means you can create a WebView based component having index.html as the view, index.jslocal as the controller (javascript) and index.css for the view’s style. Read the documentation to see how powerful it truly is.

Of course I’m gonna make use of moment.js. I am going to add some startup logic in Core so I’ll include those (empty files for now). One of the most important, the routes, so I’ll go ahead and require routes.js, I will get to it again later.

var Moment = require('alloy/moment');var Core = require('core');// Routes to navigate the app
require('routes');
// Finally, registering notifications.
Notifications.onReceived = function(e) {
// Handle notifications
};

At this point we’re missing core.js so let’s create it. It must be created in ./app/lib/core.js

I’ll also add another file called routes in ./app/lib/routes.js which I mentioned above.

To explain better what core.js is used for, I’ll add a couple of functions to it.

exports.notification = null;
exports.canConsumeUINotifications = false;
exports.canConsumeQueuedNotification = function() {
if (exports.notification == null) return false;
};
exports.consumeQueuedNotification = function() {
if (exports.notification == null) return;
var notif = _.clone(exports.notification);
exports.notification = null;
Logger.debug(">>> Notification: ", notif); // App events are defined in events.

if (notif.inBackground == false && notif.data.alert != null) {
Event.trigger('ui.message', notif.data.alert);

} else if(notif.data.data && notif.data.data.route != null) {
// Using Trimethyl router
Router.go(notif.data.data.route);

} else if (notif.data.data && notif.data.data.external_link != null) {

Util.openHTTPLink(notif.data.data.external_link);
}
Event.trigger("notifications.new", notif);
};

Here we define canConsumeQueuedNotification and consumeQueuedNotification with the first returning true if there are new notifications and the second acting accordingly to the notification payload.

If the payload contains an alert text, a global event is triggered. You can imagine what that does and we will .

Remember to add var Util = T('util'); and var Event = T('event') in alloy.js

Util has some utility functions such as the one used above openHTTPLink which makes all the necessary check and then opens the link making use of the safaridialog module if available and if it’s not, the link will be opened using safari on iOS and the default web browser on Android.

I’ll explain later where I’ll handle events.

Configuration

I set up the project, I added the missing files like routes.js (although still empty), core.js but before I start with the coding, I’m going to set some configuration settings in ./app/config.json which is a configuration file that Alloy automatically includes in the project.

I’ll add some global configuration settings and then some environment specific ones. The global settings aren’t relevant, you can check them on github. The environment specific settings which also define the configuration for Trimethyl are as follows

"env:development": {
"T":{
"notifications":{
"driver": "http"
},
"http":{
"base": "http://imdb.api.progress44.com",
"log": false
},
"ga": {
"ua": "UA-0000-00",
"log": false
},
"fb":{
"permissions":["email","user_likes"]
},
"auth":{
"facebook":{
"loginUrl": "/login/fb"
}
}
}
}

For now it’s all the same for development, test and production environments. As you can see, the libraries are easily configurable. To better understand how to configure a particular library I want to use, I usually go to its file like ./app/lib/T/http.js and right on the heading of the file there’s an object exports.config with some defaults which can be overwritten by config.js

Routing

Trimethyl Router works very well in conjunction with Flow but it can be used for all sorts of things.

With the Router object you can emulate the routing concept adopted in many web framework, like Laravel, Symphony or Expressjs. Basically, with this paradigma you associate a string (route name) with a callback function that (most of the time) open a Window in your app.
Docs https://github.com/trimethyl/trimethyl/wiki/Router

I’ll just explain its use through the example below. I’ll edit lib/routes.js like this

Router.on('/login', function() {
Flow.open("index", {});
});

Router.on will be triggered when you go to that route using Router.go as I wrote above. If there’s a match, the function(s) will be executed and you can have middlewares too. Router.on works with regular expressions as well and the matched groups are passed to the functions as parameters.

Flow is a utility helpful for managing window stacks. I am going to initialise it by setting a main navigation window in index.js

Flow uses a concept of navigation though the windows. Once defined a base navigator, it automatically tracks all opened/closed windows (tracking them through Trimethyl.GA module).
Docs https://github.com/trimethyl/trimethyl/wiki/Flow

Flow.setNavigationController($.index, true);

And the content of index.xml

<Alloy>
<NavigationWindow module="T/uifactory">
<Window id="mainWindow">
<!-- some content -->
</Window>
</NavigationWindow>
</Alloy>

The navigation window component will extend from UIFactory which I just added with the command trimethyl add uifactory && trimethyl install

I also have to add var UIFactory = T('uifactory'); if I need to use the library from code.

This is what I’ve got so far. Check out this commit

To get to this result I had to make some considerations (commit message explains it as well). In order to use Facebook login, I need to set the fbproxy to the root ui controller. What I did is to set index.xml to an empty window and add the following to the controller

var args = arguments[0] || {};//////////
// Open //
//////////
function auto() {
Router.go("/movies");
}
if (OS_ANDROID) {
UI.RootActivity = $.getView();
UI.RootActivity.fbProxy = FB.createActivityWorker({ lifecycleContainer: UI.RootActivity });
UI.RootActivity.addEventListener('open', auto);
UI.RootActivity.open();
} else {
auto();
}

When I’ll implement authentication, auto will also be the function to automatically login the user and go to movies or go to the login view if they are not logged in.

Now routes.js has changed as well. Being out of a navigation flow I can’t use Flow.open but I need to use Flow.openDirect therefore the updated routes.js file will be

Router.on('/login', function() {
// index isn't the login window anymore
Flow.openDirect("login", {});
});
Router.on('/movies', function() {
Flow.openDirect("movie", {});
});

Login with Facebook

To make this work, first I need to register an app on https://developers.facebook.com/

Facebook developers website

iOS

Facebook Login block

Once I completed this simple form and the captcha that follows, my Facebook app was completed and now I can implement the Facebook SDK.

The developers console shows me a set of services I can choose to set up. What I’m going for is Facebook Login

Clicking on setup shows me a list of platforms I want to use this feature on. I’ll setup iOS first and then I’ll go ahead with Android.

I’ll skip to Bundle ID because with titanium, I don’t need to add the Facebook SDK, I can just use the module.

Moving on, the Facebook wizard is showing me how to update my info.plist but in the Titanium context, info.plist is incorporated in tiapp.xml

My tiapp.xml ios tag now looks like this

<?xml version="1.0" encoding="UTF-8"?>
<ti:app xmlns:ti="http://ti.appcelerator.org">
<ios>
<enable-launch-screen-storyboard>false</enable-launch-screen-storyboard>
<use-app-thinning>true</use-app-thinning>
<plist>
<dict>
<key>UISupportedInterfaceOrientations~iphone</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIRequiresPersistentWiFi</key>
<false/>
<key>UIPrerenderedIcon</key>
<false/>
<key>UIStatusBarHidden</key>
<false/>
<key>UIStatusBarStyle</key>
<string>UIStatusBarStyleDefault</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>fb1061219234012962</string>
</array>
</dict>
</array>
<key>FacebookAppID</key>
<string>1061219234012962</string>
<key>FacebookDisplayName</key>
<string>Trimethyl Test</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>fbapi</string>
<string>fb-messenger-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
</array>
</dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>facebook.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>fbcdn.net</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>akamaihd.net</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
</dict>
</dict>
</plist>
</ios>
</ti:app>

Building a cross platform app doesn’t produce a 100% compatible code as you can see. Some times you have to do things differently for each platform you’re building the app for. There are different approaches to this. I’ll explain the two ways we do it.

Either build for the less complicated platform, or the one you feel more comfortable with, first (in my case iOS) and then, when I have a stable version of that, start fixing it for the other platforms, or build every feature cross platform from the start. And this is how I’m going to do it.

Android

First things first, I’ll go back to the developer console and enable a new platform going to Settings > Basic on the left bar menu and then scrolling down.

There are very few things to add here. Google Play package name which is the same as Bundle ID for iOS. Class name is set as below (the part in bold is the class name and I’ll have to use the package name as it’s prefix)

<android xmlns:android="http://schemas.android.com/apk/res/android">
<manifest android:installLocation="auto" android:versionCode="1" android:versionName="1.0.0">
<application android:hardwareAccelerated="true" android:theme="@style/Theme.AppCompat" android:largeHeap="true">
<activity android:name=".TrimethylTestActivity" android:screenOrientation="portrait" android:configChanges="keyboardHidden|orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
</android>

My class name will be com.caffeina.trimethyltest.TrimethylTestActivity
If there are problems with this step, check AndroidManifest.xml under build/android for the correct classname.

If your appname and android:name differ you’ll see two icons of your app on the phone and both will launch it.

I also need to add a couple of activities and a meta data in the application node of the tiapp.xml file.

<activity 
android:name="com.facebook.Activity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:label="Trimethyl Test"
/>
<activity
android:name="com.facebook.FacebookActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:label="Trimethyl Test"
android:configChanges="keyboard|keyboardHidden|screenSize|orientation"
/>
<meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>

And the meta data references string/facebook_app_id which I must add in /app/platform/res/values/strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_id">1061219234012962</string>
</resources>

Now I have to make my keyhashes. What is an Android key hash?

keytool -exportcert -alias androiddebugkey -keystore "~/Library/Application\ Support/Titanium/mobilesdk/osx/6.0.4.CAFFEINA-1/dev_keystore" | openssl sha1 -binary | openssl base64

This is the resulting hash key rUv+KchXF0iiWn2shxp3igTr+PE=

Since on Android it’s pretty easy, I’ll just create the productionkeystore and hash right now.

keytool -genkey -keystore "./certs/keystore" -storepass "PASSWORD" -keypass "PASSWORD" -alias "TRIMETHYL" -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=TRIMETHYL, OU=Mobile, O=Caffeina, L=Parma, S=Parma, C=IT"

The above command will generate a file name keystore in the app directory under certs with storepass and keypass set to PASSWORD. I set the alias of the keystore to TRIMETHYL in this case but usually I set it equal to the app name. Same goes for the certificate name (CN).
The command below instead will output the production hash to be copied to Facebook. Obviously the alias, keystore path and password must be the same as above.

keytool -exportcert -alias "TRYMETHIL" -keystore "./certs/keystore" -storepass "PASSWORD" | openssl sha1 -binary | openssl base64

The last step is to set the fbproxy to the main activity but I’ve already done this in the index.js controller above.

Check this commit for the full tiapp.xml

UI

If I was developing the app natively I’d have to do all the above, what changes is the following part.
Now that I have everything I can make my login button.

I’ll create my login controller and its view

Not going to style it too much

The above is my login view, the controller is empty for now.

All I have to do to have Facebook login working on my app now is the following lines of code.

$.loginButton.addEventListener("click", function() {
Auth.login({ driver: 'facebook' });
});

That’s exactly 3 lines of code

Auth initialises the Facebook module, asks for authorisation by the user and once the access_token has been received, it’s sent to the server automatically on the endpoint login/fb that can be configured very easily.
Unfortunately I haven’t worked on my API yet so it returns an error and the login isn’t successful. But Facebook login works.

Handling response

Fair enough, but how do I tell it what to do when the login is successful or how to handle errors?

Movies list after logging in (both success or error cases)

Auth triggers a few global events. Now I’ll add an events.js file /lib/events.js to manage global events. I’ll add some handlers to it for the authentication. I’ll require it in alloy.js right under require('routes');

The events Auth triggers are success error logout I’ll add a handler for the first two.

function authHandler(e) {
Logger.debug('Auth: ', e);
Flow.close();
Router.go("/movies");
}
Auth.event('success', authHandler);
Auth.event('error', authHandler);

I’m using the same function for both success and error, because my login api doesn’t work but I want to be able to use the app nonetheless. That’s why Router.go("/movies");

Try it yourself.

Fetching data from the server

(Maybe I’ll write another article about the development of a simple REST API service 😀)

I’ve created a simple API service to expose a few of the routes needed for my application.

The notifications module needs these four endpoints

  • /notifications/subscribe
  • /notifications/unsubscribe
  • /notifications/mute
  • /notifications/unmute

They mean respectively, subscribe the current device to receiving push notifications, unsubscribe it, leave the device subscribed but the server shouldn’t send any notifications and return to the subscribed state.
These can be overwritten in the config.json file in T >notifications > http

To enable authentication, you’ll need to expose these APIs in your server.

  • /login
  • /login/fb — this is called automatically when logging in with Facebook
  • /logout

No need to explain what they do. Obviously you can override these in conf.json
In my server-side application, all of the above return {success: true}

I implemented instead 3 API endpoints to continue with my app. These are

  • /movies — Obvioulsy returns a collection of movies
  • /movies/:id — Returns a movie model identified by id
  • /tmdb/configuration — Has some important settings to compose the images url

To respect these choices I need a configuration and movie Alloy models. I’ll start with the movie model.

Alloy models (and collections) are an extension of Backbone.js models. They work exactly the same. Trimethyl will include automatically a few sync implementations so the Models are linked to a REST service out of the box.

exports.definition = {
config: {
adapter: {
type: 'api',
name: 'movies'
}
},
extendModel: function(Model) {
_.extend(Model.prototype, {

});
return Model;
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {

});
}
};

I don’t really need to add more to it. The configuration model is really similar changing only in the name.

exports.definition = {
config: {
adapter: {
type: 'api',
name: 'tmdb/configuration'
}
},
extendModel: function(Model) {
_.extend(Model.prototype, {

});
return Model;
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {

});
}
};

Since I have to arrange the images base url every time, I’m going to create a method for it in the configuration model.

The method I wrote is the following

getImageBase: function(size, type) {
var img = this.get('images');
if (!img || img.length == 0) return;

// getting sizes for the image type
// and filtering them by the closest
// or return the latest in the list
return img.secure_base_url + _(img[type + "_sizes"]).filter(function(s) {
// return true if the absolute value of the
// size - requested size is less than or equal to 20
return Math.abs( parseInt( s.substr(1) ) - size ) <= 30;
}).join() || img[type + "_sizes"][img[type + "_sizes"].length - 1];
}

Check out this file to see how it all comes together. To get a better understanding, check out what the API returns for this call.

I also added a utility in core.js (lib/core.js) to get the configuration from the API.

Learn more about Q and promises here.

var configuration = null;exports.getConfiguration = function() {
return Q.promise(function(resolve, reject) {

if (configuration == null) {
configuration = Alloy.createModel("configuration");

configuration.fetch({
success: function() {
resolve(configuration);
},
error: reject
});

} else {

return resolve(configuration);

}
});
};

This utility returns the configuration model after fetching it from the server.
Last but not least, I updated routes.js so I download the configuration before opening the new window like below.

Router.on('/movies', function() {
Core.getConfiguration()
.then(function(conf) {

Flow.openDirect("movie", {tmdb: conf});

});
});

So now when the “Movie” view opens, I’ll have args.tmdb populated with my configuration. I can fetch the movies collection from the API, in the view’s controller.

var args = arguments[0] || {};
var Movies = null; // New line
Flow.setNavigationController($.movie, true);// Init
function init() {
Movies = Alloy.createCollection("movie");
Movies.fetch({
success: function() {
$.list.setConfiguration(args.tmdb);
$.list.setCollection(Movies.filter(function(movie) {
return movie.get('poster_path');
}));
},
error: function(err) {
Logger.error(err);
}
});
}
init();

I added a variable Movies and the block init which fetches the collection. To show the list of movies I’ll use a ListView and bind the collection through a Trimethyl utility. But in my movie.xml all the content of the window is being required from another controller in “controllers/movies/list” so it’s here that I have to implement the functions setCollection and setConfiguration to pass the collection from the movie.js controller to movies/list.js controller.

My list.xml view, contains now a ListView component with a single template.

In the list.js file, the two functions mentioned above are very simple, they only save the data in the arguments to a variable. What I need to add now is two more functions. I’m going to call the first function populate and this, as the name says will add the data to the ListView. Here’s the code for it.

function populate() {
UIUtil.populateListViewFromCollection(Movies, {
datasetCb: getTemplateObject
}, $.moviesList);
}

Very simple right? There’s one last thing. As you can see, there’s a property named datasetCb passed to that function. It’s set to another variable which in fact is the second function I have to implement. As you can see below, all it does is return an object that reflects the ListView template and uses the model data.

function getTemplateObject(model) {
return {
poster: {
image: Config.getImageBase(200, "poster") + model.get("poster_path")
}
};
};

To try it to this point checkout this commit. Here’s the outcome.

iOS simulator showing a list of movies

There’s one thing left to do to move on to the next step. I need the app to show me the information of the movie I’m tapping on so I’ll add an event listener to the ListView items to open the Movie view for the tapped item.

function listItemClicked(e) {
if (e.itemId) Router.go("/movie/" + e.itemId);
};

The function that I want to be called when the the tap event is triggered is the one above which simple will tell the router to go to the movie with a certain id

To have the function triggered I need to add it to the list view. In the item template element named movie I have a sub view with id columns and I’m going to add the listener as below

<ItemTemplate name="movie">
<View id="columns" onClick="listItemClicked">
<ImageView bindId="poster" id="poster"/>
...

And now I can consider the movies list view complete. Moving on to the next task, the single movie view.

Completing the app

The most important part of the app for the user would be the single movie view where they can see all the information about a certain movie. Simple.

This is very easy to achieve as I don’t have to use rocket science but just compose the view putting all the pieces together. It’s more a work of design than coding.

Here is the resulting view I want.

Movie view, cover on the left and below the fold on the right

I want the movie poster to take most of the screen and then scrolling down I want to show some information about the movie. Since I’m certain I will have to scroll the view because there’ll be a lot of content, I will use a ScrollView

As you can notice from my code, I make extensive use of the layout property. This way I don’t have to set fixed positions on view elements and everything is adaptable. (It may even work in landscape mode 🤥)

The code for the view itself is very small.

<Alloy>
<Window title="Movie">
<ScrollView id="container">
<View id="coverContainer">
<ImageView id="cover"/>
</View>
<View id="info">
<Label id="title" />
</View>
</ScrollView>
</Window>
</Alloy>

Strangely it only contains the cover image and the title. I did this because I wanted to add the other information dynamically.

But first, let’s get back to the routes.js and update it so it fetches the content before opening the movie view.

Router.on(/\/movies\/([^\/]+)\/?/, function(id) {
Core.getConfiguration()
.then(function(conf) {
return Q.promise(function(resolve, reject) {
var movie = Alloy.createModel("movie", {id: id});
movie.fetch({
success: function(m) {
resolve([conf, movie]);
},
error: reject
})
});
})
.then(function(data) {
Flow.open("movie/single", {
id: id,
tmdb: data[0],
movie: data[1]
});
})
.catch(function() {
alert("There has been an error while loading the movie or the configuration");
});
});

I also added a fallback function to catch exceptions. It will show an alert if anything goes wrong

So what happens, the movies list view gets a tap event on a movie and it calls the router. The router, reloads the configuration from the API and fetches the movie data. If both these actions succeed, the router will open the movie view.

Now the movie controller will be executed. I wrote an init function to start things up. This function does 2 things, it checks that the configuration exists in the controller arguments (these are the arguments passed by the router), and secondly will check that the movie model is being passed as well in the arguments.

// init
function init() {
$.setConfiguration(args.tmdb);
$.setModel(args.movie);
}
init();

If these arguments are empty, both methods setModel and setConfiguration will try to load them again from the server.

I’m aware that there is a flaw in the implementation above. Making server requests is asynchronous, yet I call the methods one after the other without using Promises like I did in the router. Well the reason for this is that developers are lazy. Being this not a production app, I can be lazy and assume it won’t fail.

The setConfiguration method just saves the argument in a variable global to the controller. setModel instead, does an extra thing. It calls populate which will fill the view with information.

Below the populate method.

function populate() {
$.cover.image = Config.getImageBase(500, "poster") + Movie.get("poster_path");
$.title.text = Movie.get('original_title'); $.info.add(createRow("Original Language", [ Movie.get('original_language') ]));
$.info.add(createRow("Release Date", [ Movie.get('release_date') ]));
$.info.add(createRow("Popularity", [ Movie.get('popularity') ]));
$.info.add(createRow("Genre", pickName(Movie.get('genres')) ));
$.info.add(createRow("Production Company", pickName(Movie.get('production_companies')) ));
$.info.add(createRow("Production Countries", pickName(Movie.get('production_countries')) ));
$.info.add(createRow("Plot",[ Movie.get('overview')] ));
}

First thing, setting the cover image. Then it sets the title of the movie. In the end, a lot of similar rows. There I expect the createRow method to give me a view object that I can add inside the info object in my View. This method is not really necessary. I made it, so I wouldn’t add all the rows of information statically in my View. What it does is create a Label object with a hint on the information that’s about to follow, and a list of data which represent the information about the movie.

Here’s the commit with the code for this section.

This would be the last step to the development of the app.

One last step, publishing the app

A checklist before building for production.

  • Version number (both apple and google don’t allow you to upload the same build twice)
  • Correct Google Analytics code
  • Correct API endpoint for the production environment
  • ATS (or better yet, that you’re in HTTPS with SSL pinning enabled)
  • Maybe, just to be sure, check the app on different screen sizes, just maybe.
  • Check appicon and splash image of course
  • Check assets exist for all screen resolutions (2x, 3x, hdpi, xxhdpi and so on)
  • Make sure to have a DefaultIcon file in the app’s root directory so Titanium can generate icons if any are missing.
  • Merge feature branch into develop, check everything, have your code reviewed, merge develop into master, double check you didn’t forget anything, fix merge conflicts, push to master, tag version. Eventually configure the CI to build and release the app.
  • Build for production.

Not so fast

Before building the app I must make sure of some things. For Android I need the production keystore (I already explained how to get this above). For iOS the process is a little longer. Assuming I have an Apple developer account, I have to do the following.

(To test the app in a device during development I would have to do the same process again)

  • I have to go to developer.apple.com and login
developer.apple.com dashboard
  • Certificates and profiles is what I need so that’s what I’m clicking.
  • On the top left there’s a section about certificates. I’m selecting production there.
Top left of the sidebar, certificates section
  • I’ll click on the plus on the top right and then just follow the wizard step by step. It’s pretty simple.
Certificate creation
  • Now that I have the certificate I’m going to add it to keychain. Don’t have to worry about that for a year
  • Next step, provisioning profile, left sidebar again but this time it’s the bottom block.
Bottom block of the left sidebar, provisioning profile
  • Last, plus button again and just follow the wizard to create the provisioning profile
Provisioning profile creation

Finally I can build the app for production :D

tn appstore -output-dir “dist” -no-prompt -log-level warn
tn playstore -output-dir “dist” -no-prompt -log-level warn

The above commands will output the two built apps (ipa, apk) in the app’s dist folder.

To publish it I have to go to Android developer console and iTunes connect. Not going to cover publishing in this article.

Some more advantages of using Trimethyl

Google Analytics automatically tracks screen views, uncaught exceptions and events
Sentry, automatic logging of exceptions
Seamless integration with HTTPS/SSL, SSL Pinning

View, is an xml file representing the layout of UI elements.
Controller, is a javascript file that has the code relative to a View.
Style, is a tss file which contains all the styles of a View.
Every single UI element can be called a View.

You can download the code of the app on github https://github.com/progress44/trimethyl-guide

If you have any questions, comment or open issues on github.

Thanks for reading this far and stay tuned for more fun articles :)

--

--