Env
Application type: mobile Titanium SDK: 3.2.1GA Platform & version: iOS 7.0, Android 4.0+ Device: iOS simulator, Android emulator Host Operating System: OSX Mountain Lion Titanium CLI: 3.2.1
Problem
Only been using Titanium for 2 weeks. Searched long and hand and am now thoroughly confused.
Using the above env, Alloy 1.3.1.
In a nutshell this is what I am doing:
- php backend serving up data in JSON format
- app consumes the data into a collection
- collection.reset(json object) bind the collection to tableview
So far so good.
Now I am trying to implement offline mode
- save the collection to sqlite database on device
- check for network connection
- if offline, fetch the collection from sqlite
- if online, fetch from php backend and merge (REPLACE INTO) new results into sqlite database
This is where I am stuck.
I've searched for days but all of the examples talk about 2 ways RESTful syncing.
I don't want to update the remote source, only update the local sqlite database.
This seems like a fairly common task that others must be doing.
I think I am missing something fundamental, and need some advice
I found one example which I have tried to implement as follows:
- As far as I can tell, you cannot save a collection in backbone.js. Therefore I've extended the collection adding a "save" function that loops through the models and saves it
- Looks like there's a bug in the documentation for db.execute - on Andriod you can pass an array and it works, but on iOS it fails
Questions
- Is the approach of adding a save function to the collection the way to save results locally to sqlite?
- If so is there a more efficient way to save an entire collection rather than my implementation?
- Is there a workaround for not being able to pass an array into db.execute on iOS?
- If the answer to 1) is no, then how do you do it?
- Once the data is saved to sqlite, is the only way to get it into a collection to use collection.fetch()?
- If 5) is yes, then the backbone.js document say you should avoid this and use collection.reset() - but how would this work for sqlite in practice?
Sorry for the rambling, long list of questions.
Wanted to stop and seek advice, before I code some inefficent non best practice mess.
Thanks in advance.
Steve
Code
model:
exports.definition = { config : { // table schema and adapter information "columns": { "event": "TEXT", "event_link": "TEXT", "men_runner": "TEXT", "men_runner_link": "TEXT", "men_athlete_number": "TEXT", "men_club": "TEXT", "women_runner": "TEXT", "women_runner_link": "TEXT", "women_athlete_number": "TEXT", "women_club": "TEXT" }, "defaults": { "event": "-", "event_link": "-", "men_runner": "-", "men_runner_link": "-", "men_athlete_number": "TEXT", "men_club": "-", "women_runner": "-", "women_runner_link": "-", "women_athlete_number": "TEXT", "women_club": "-" }, "adapter": { "type": "sql", "collection_name": "first_finishers" } }, extendModel: function(Model) { _.extend(Model.prototype, { // Extend, override or implement Backbone.Model }); return Model; }, extendCollection: function(Collection) { _.extend(Collection.prototype, { // Extend, override or implement Backbone.Collection deleteAll : function() { var collection = this; var sql = "DELETE FROM " + collection.config.adapter.collection_name; db = Ti.Database.open(collection.config.adapter.db_name); db.execute(sql); db.close(); collection.trigger('sync'); }, save: function(){ var collection = this; var numModels = collection.length; Ti.API.info("Number to process: " + numModels); //build comma separated list of columns Ti.API.info("columns " + JSON.stringify(collection.config)); var collectionColumns = _.map(collection.config.columns, function(value, index) { //ignore the system generated pk - alloy_id if (index !== 'alloy_id') return [index]; }); //pop off the trailing array item because we don't return alloy_id collectionColumns = collectionColumns.filter(function(n){return n}); Ti.API.info("columns " + collectionColumns.length + " - " + collectionColumns.join(", ")); //Open the DB db = Ti.Database.open(collection.config.adapter.db_name); //Start the transaction db.execute('BEGIN'); //Build the SQL statement based on this collection collection.each(function(index,model){ var sql = 'REPLACE INTO ' + collection.config.adapter.collection_name + ' (' + collectionColumns.join(", ") + ') VALUES ' //loop through all columns and set a bind placeholder var modelBinds =_.map(collectionColumns, function(value, index) { //Ti.API.info('collectionColumns : index = ' + index + ', value = ' + value); return ["?"]; }); //clean the array of emtpy values modelBinds = modelBinds.filter(function(n){return n}); //add this to the SQL statement sql += '(' + modelBinds.join(", ") + ')'; //loop through all columns and get the model data for that column var modelValues =_.map(collectionColumns, function(value, index) { //Ti.API.info('collectionColumns : index = ' + index + ', value = ' + value); return ["'" + collection.at(model).get(value) + "'"]; }); //clean the array of emtpy values modelValues = modelValues.filter(function(n){return n}); //execute the SQL //this // db.execute(sql, modelValues); //should work on iOS according to the docs - http://docs.appcelerator.com/titanium/latest/#!/api/Titanium.Database.DB-method-execute //but it does not //allow it to work on android as its neater, but hack a workaround for iOS if (OS_ANDROID){ db.execute(sql, modelValues); } else { //http://developer.appcelerator.com/question/119216/so-arguments-apply-and-call-doesnt-work-with-titaniumdatabaseexecute //https://developer.appcelerator.com/question/60461/passing-an-array-into-titaniumdatabasedbexecute function query(stmt, args) { var resultSet = Function.apply.call(db.execute, db, arguments); return resultSet; } //this DOES NOT WORK?! } }); //Commit the transaction db.execute('COMMIT'); //Close the DB db.close(); Ti.API.info('collection sync - save'); } }); return Collection; } };
controller:
var ffs = Alloy.Collections.first_finishers; var isIpad = OS_IOS && Alloy.isTablet; Alloy.CFG.nav=$.nav; var Net = require('call_webservice'); Net.download({ method: 'GET', // GET is default url: 'my_webservices/getFirstFinishers.php', timeout: 30, // in seconds (10 = default) type: 'json', // is default or html success: function (success) { Ti.API.info('Successfully got the response!'); //Clear the model down //ffs.deleteAll(); ffs.fetch(); Ti.API.info('ff collection length before reset = ' + ffs.length); //bind this data to the model ffs.reset(success); //ffs.add(success); <--way to slow //save this to sqlite ffs.save(); Ti.API.info('ff collection length after reset = ' + ffs.length); Ti.API.info('JSON.stringify(ff) = ' + JSON.stringify(ffs)); Ti.API.info('arh men_athlete_number = ' + ffs.at(0).get("men_athlete_number")); Alloy.Globals.loading.hide(); } }); // respond to detail event triggered on index controller $.on('newview', function(e) { Ti.API.info('start ON newview'); // open the window var xpng=require('xpng'); xpng.openWin(Alloy.CFG.nav, 'newview', { athleteNumber: ffs.at(e.index).get("men_athlete_number"), menRunner: ffs.at(e.index).get("men_runner") }); Ti.API.info('end ON newview'); }); //$.index.open(); if (OS_ANDROID){ $.index.open(); } else { $.nav.open(); } function clicked(e) { Ti.API.info('clicked row ' + e.index); Ti.API.info('Collection at index[' + e.index +'], event = ' + ffs.at(e.index).get("men_runner_link")); $.trigger('newview', e); } // Free model-view data binding resources when this view-controller closes if (OS_ANDROID){ $.index.addEventListener('close', function() { $.destroy(); }); } //else for iOS nav group?