Quantcast
Channel: Appcelerator Developer Center Q&A Unanswered Questions 20
Viewing all articles
Browse latest Browse all 8068

Save remote JSON data into SQLite for offline use

$
0
0

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:

  1. php backend serving up data in JSON format
  2. app consumes the data into a collection
  3. collection.reset(json object) bind the collection to tableview

So far so good.

Now I am trying to implement offline mode

  1. save the collection to sqlite database on device
  2. check for network connection
  3. if offline, fetch the collection from sqlite
  4. 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:

  1. 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
  2. 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

  1. Is the approach of adding a save function to the collection the way to save results locally to sqlite?
  2. If so is there a more efficient way to save an entire collection rather than my implementation?
  3. Is there a workaround for not being able to pass an array into db.execute on iOS?
  4. If the answer to 1) is no, then how do you do it?
  5. Once the data is saved to sqlite, is the only way to get it into a collection to use collection.fetch()?
  6. 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?

Viewing all articles
Browse latest Browse all 8068

Trending Articles