Alloy Collections vs. Raw SQLite: Picking a Data Strategy for Titanium List Screens
Choosing between Alloy data binding and direct Ti.Database queries for Titanium list screens involves trade-offs in boilerplate, UI synchronization, and query complexity. Here is how to decide.
15 May 2026, 05:03 UTC

The list-screen fork in the road
You are building a screen that shows a scrollable list of records in an Appcelerator Titanium app using the Alloy framework. The immediate question is not whether to use a database, but how to connect it to the UI. Alloy gives you a declarative binding path through Backbone-style models and collections. Titanium also gives you direct SQLite access through Ti.Database. Choosing the wrong path early means either fighting the framework or maintaining fragile SQL inside controllers.
The practical takeaway: use Alloy collections when your list is a straightforward projection of a single table or a simple adapter result, and the dataset is small to moderate. Reach for raw Ti.Database when your query involves multi-table joins, aggregations, or result sets large enough that per-object synchronization becomes a bottleneck.
What Alloy binding actually does
Alloy layers an MVC structure on top of Titanium. Models are Backbone-based objects that know how to fetch, save, and destroy records. A collection is an ordered set of those models. In an Alloy XML view, you can attach a collection to a UI component using the dataCollection attribute on a ListView or TableView. When the controller calls collection.fetch(), the adapter reads the records, the collection triggers change events, and the bound UI updates without you manually creating ListItems.
Under the hood, Alloy relies on a sync adapter to move data between the collection and a persistence layer. The default local adapter uses SQLite, but the architecture also supports HTML5 localStorage and custom backends. That abstraction is useful: you can prototype with local storage and later point the same controller at a REST API by changing the adapter configuration, not the view markup.
Worked example: a bound book list
Assume you have an Alloy project and want to display books stored locally. First, scaffold the model and collection so the file structure stays consistent. These commands run from your project root and require the Appcelerator CLI to be installed and authenticated; they will create new files under app/models/, so review the generated output before committing it.
appc alloy generate model book title:string author:string published:date
appc alloy generate collection book Book
Next, define a ListView in app/views/index.xml that binds to the collection:
<Alloy>
<Window>
<ListView id="bookList" dataCollection="book">
<ListSection>
<ListItem title="{title}" subtitle="{author}" />
</ListSection>
</ListView>
</Window>
</Alloy>
In app/controllers/index.js, fetch the collection. The binding handles the rest:
var books = Alloy.Collections.book;
books.fetch();
Expected check: after fetch() resolves, the ListView should render one item per book without any imperative row insertion. If the list stays empty, verify that the model's adapter configuration in app/models/book.js points to a table that actually contains rows, and that column names match the model attributes.
When raw SQLite is the better tool
Alloy collections shine for CRUD screens, but they add overhead when your data access pattern is not a simple table scan. If you need a query that joins three tables, calculates aggregates, or applies window functions, expressing that through Backbone model methods becomes awkward. At that point, querying Ti.Database directly is often clearer and faster.
The trade-off is real: you lose automatic UI synchronization and the migration tooling that alloy generate provides. You also take on the responsibility of closing result sets and managing transactions. For very large lists—say, thousands of rows—the per-model object graph and change-event propagation can introduce latency. In those cases, a manual Ti.Database query feeding a ListView with a dataFunction or paging logic gives you tighter control over memory and scroll performance.
There is also a debugging cost to binding's convenience. Because the adapter and collection handle fetching behind the scenes, it can be harder to see whether a slow screen is waiting on disk I/O, a network call, or model serialization. A raw query sitting visibly in a controller is often easier to profile.
A pragmatic decision matrix
| Scenario | Recommended approach | Why |
|---|---|---|
| Single-table list with create/edit/delete | Alloy collection binding | Minimal boilerplate; UI syncs automatically on fetch and model changes |
| Complex relational report or aggregate view | Raw Ti.Database query |
SQL expresses joins and grouping more directly than Backbone model composition |
| List of 1,000+ rows with images | Manual query + ListView paging or dataFunction |
Reduces object-graph overhead and scroll jank compared to bound collections |
| Prototype that may move to a REST API | Alloy collection with swappable sync adapter | Persistence strategy changes in config, not in controller logic |
Limitations and verification
Alloy's binding behavior and Backbone integration have shifted across Titanium SDK versions. The dataCollection syntax shown above reflects common Alloy patterns, but you should confirm exact attribute names and adapter options against the SDK version pinned in your tiapp.xml. The Appcelerator ecosystem has also changed ownership and distribution over time; verify that the CLI and SDK packages you need are still available before starting new work.
To check whether binding is actually helping, build a minimal screen with 100 records using both paths. Measure time-to-first-render and scroll smoothness. If the bound version feels sluggish or you find yourself writing SQL strings inside model definitions anyway, that is a signal to move that screen to direct database access.
Bottom line
Alloy collections are the right default for conventional list screens because they keep data logic out of your view controllers and make persistence swappable. They are the wrong choice when query complexity or dataset size makes the abstraction expensive. Start with binding, profile with realistic data volumes, and drop to Ti.Database only where measurements justify it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.