Appcelerator Alloy MVC: Data Binding, Platform-Specific Views, and Common Pitfalls
Alloy gives Titanium apps MVC structure with zero runtime cost, but data binding is one-way by default, platform files resolve at compile time, and debugging means reading generated code. Includes a complete Book list example with model, view, controller, TSS styling, and verification steps.
26 Apr 2026, 12:40 UTC

The Problem: Structured Native Apps Without Runtime Overhead
Appcelerator Titanium compiles JavaScript to native iOS and Android code, but raw Titanium Classic APIs leave UI, logic, and styling tangled in single files. Alloy solves this with an MVC layer that compiles to standard Titanium JavaScript at build time — zero runtime cost, native performance, and a familiar separation of concerns. The catch: data binding is one-way by default, platform-specific files are resolved at compile time (not runtime), and debugging means reading generated code in Resources/alloy/controllers/ because source maps are limited.
How Alloy Fits Together
An Alloy project uses three directories under app/:
models/— Backbone.js models and collections (data layer)views/— XML markup for UI (presentation layer)controllers/— JavaScript logic tied to each view (behavior layer)styles/— TSS files (JSON-like styling with platform/media queries)
At build time (ti build), the Alloy compiler merges these into Resources/ as plain Titanium JavaScript. No Alloy runtime ships with your app.
Worked Example: Book List with Data Binding
This example shows a model persisted to local properties, a controller that creates and binds the model, and a view that renders a ListView with automatic updates when the model changes.
1. Model — app/models/book.js
exports.definition = {
config: {
columns: {
title: 'string',
author: 'string'
},
adapter: {
type: 'properties' // persists to local JSON file
}
},
extendModel: function(Model) {
_.extend(Model.prototype, {
// custom methods here
});
return Model;
},
extendCollection: function(Collection) {
_.extend(Collection.prototype, {
// custom methods here
});
return Collection;
}
};
The properties adapter writes a JSON file to the app's private storage. For SQLite, change type: 'sql' — but schema migrations are manual.
2. View — app/views/index.xml
<Alloy>
<Window id="win" title="Books">
<ListView id="listView" defaultItemTemplate="book">
<Templates>
<ItemTemplate name="book">
<Label bindId="title" data-bind="text: title" class="bookTitle" />
<Label bindId="author" data-bind="text: author" class="bookAuthor" />
</ItemTemplate>
</Templates>
<ListSection id="section" />
</ListView>
<Button id="addBtn" title="Add Book" onClick="addBook" />
</Window>
</Alloy>
Key points:
data-bind="text: title"connects the model'stitleproperty to the label'stextproperty.bindIdmaps the view element to the model attribute forbindCollection.defaultItemTemplateonListViewavoids repeating the template name per item.
3. Controller — app/controllers/index.js
var book = Alloy.createModel('book', {
title: 'Titanium Guide',
author: 'Appcelerator Docs'
});
book.save(); // writes to properties store
// Bind model to ListView section
$.listView.bindCollection(book);
// One-way binding: model → view updates automatically
// For view → model, add explicit event listeners
function addBook() {
var newBook = Alloy.createModel('book', {
title: 'New Book ' + Date.now(),
author: 'You'
});
newBook.save();
// Collection binding picks up the new model automatically
}
// Trigger two-way update from UI
$.titleField.addEventListener('change', function(e) {
book.set('title', e.value); // model updates, view refreshes via binding
});
$.win.open();
Run this on device or simulator:
# From project root (requires Titanium CLI and SDK)
ti create -t app -n BookApp --alloy
cd BookApp
# Replace generated app/ files with the three files above
ti build -p ios # or -p android
Where to run: Terminal with Titanium CLI installed (npm i -g titanium or via Appcelerator Studio). Permissions: Standard user; iOS build requires macOS with Xcode. Expected check: App launches, shows one book, "Add Book" button inserts a new row without manual ListView refresh. Risk: properties adapter data persists across reinstalls on simulator; clear app data to reset.
Platform-Specific Files: Compile-Time Resolution
Alloy selects platform-specific files at compile time using suffixes:
views/ios/index.xml— used only for iOS buildscontrollers/android/index.js— used only for Android buildsstyles/ios/index.tss— iOS-only styles
Changing Ti.Platform.osname at runtime has no effect — the wrong file is simply not compiled in. To verify, add console.log('iOS view loaded') in views/ios/index.xml's controller and build for Android; the log will not appear.
TSS Styling with Media Queries
TSS uses JSON-like syntax with selectors. Platform-specific overrides use @media blocks:
// app/styles/index.tss
"#addBtn": {
width: 200,
height: 44,
backgroundColor: '#007AFF',
color: '#FFF'
},
"@media[os=android]": {
"#addBtn": {
backgroundColor: '#4CAF50',
height: 48 // Material Design touch target
}
},
".bookTitle": {
font: { fontSize: 16, fontWeight: 'bold' },
color: '#333',
left: 10, top: 5
},
".bookAuthor": {
font: { fontSize: 14 },
color: '#666',
left: 10, top: 25
}
Selectors support IDs (#id), classes (.class), and element types (Label). The @media query evaluates at compile time, so Android builds get the green button, iOS gets blue — no runtime branching.
Controller Communication
Two patterns cover most navigation and messaging needs:
Navigation with Parameters
// In a list item click handler
var detail = Alloy.createController('detail', {
book: selectedBook // pass model or plain object
});
detail.getView().open(); // opens the detail window
The detail controller receives arguments in its args parameter: var book = $.args.book;.
Decoupled Events
// Publisher (e.g., a settings controller)
$.trigger('themeChanged', { darkMode: true });
// Subscriber (e.g., root controller)
$.on('themeChanged', function(data) {
applyTheme(data.darkMode);
});
Use $.off('themeChanged') to unsubscribe and avoid memory leaks when controllers are destroyed.
Limits and Common Mistakes
One-Way Binding by Default
data-bind flows model → view only. Updating a TextField does not update the model. You must attach change or blur listeners to call model.set('attr', value). The example above shows this for titleField.
Properties Adapter Is Local-Only
The properties adapter writes a JSON file per model/collection to the app's sandbox. It does not sync across devices, support queries, or handle schema changes. For anything beyond simple key-value storage, use the sql adapter and write your own migration logic.
Debugging Means Reading Generated Code
Set breakpoints in your app/controllers/ source, but the debugger may stop in Resources/alloy/controllers/index.js (the compiled output). Source maps exist but are incomplete. Add console.log in both source and generated files to trace discrepancies.
Platform Suffixes Are Not Runtime
Developers often expect views/ios/index.xml to load when Ti.Platform.osname === 'ios' at runtime. It does not — the file is excluded from the Android build entirely. If you need runtime UI branching, use a single view with platform attributes on elements or conditional logic in the controller.
Appcelerator Services Are Discontinued
Arrow (backend), Analytics, and Push services were shut down in 2022. The core Titanium SDK continues via the TiDev community fork (SDK 12+). Alloy itself is maintained in the Titanium SDK repo. Do not depend on cloud services referenced in older tutorials.
Verification Checklist
- Create a clean project:
ti create -t app -n VerifyAlloy --alloythen inspectapp/structure. - Build and inspect generated code:
ti build -p iosthen openResources/alloy/controllers/index.js— confirm your controller logic appears there. - Test binding: Add a button that calls
book.set('title', 'Updated')and verify theListViewrow updates without manual refresh. - Test platform files: Add
console.log('iOS only')incontrollers/ios/index.js, build for Android, confirm log is absent. - Test TSS media queries: Change
backgroundColorin@media[os=android]block, run on both simulators, compare screenshots.
When to Use Alloy vs. Classic
Use Alloy when:
- Team prefers MVC separation and XML declarative UI
- App has multiple screens with shared styling
- You need platform-specific UI without runtime conditionals
Stick with Classic (or consider React Native / Flutter) when:
- Project is small and single-platform
- Team wants full control over every Titanium API call
- You need hot-reload style iteration (Alloy requires rebuild)
Alloy remains the most structured way to build native iOS/Android apps with JavaScript if you accept its compile-time model and one-way binding defaults. The worked example above is a complete, minimal starting point — copy the three files into a fresh ti create --alloy project and iterate from there.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.