Angular/Ionic and async SQLite – ensuring data factory initialised before return

All we need is an easy explanation of the problem, so here it is.

I’m writing a PhoneGap/Cordova app with Ionic, and using SQLite (with ngCordova) for persistent storage. The core of the app is a scrolling list of items which are retrieved from the SQLite database.

listController.js

.controller('ListCtrl', [
  '$scope',
  'dataFactory',
  function($scope, dataFactory) {

    var items = dataFactory.getAllItems().then(function(data){
      $scope.allItems = data;
    });

  }
]);

dataFactory.js

.factory('dataFactory', [function($window, $log, $q, $cordovaSQLite, dummyDataGenerator){    

  var db_;

  // ...lots of SQLite fun in here
  // cascading async callbacks to load the database and inject dummy data
  var openDB_ = function(){...};
  var createTable_ = function(){...};
  // etc

  var getAllItems = function(){

    var q = $q.defer();
    $cordovaSQLite.execute(db_, sqlSelectString, []).then(
      function(results) {
        $log.log("SQL SELECT successful");
        var i, len, allItems = [];
        for(i = 0, len = results.rows.length; i < len; i++) {
          allItems.push(results.rows.item(i));
        }
        q.resolve(allItems);
      },
      function (err) {
        q.reject(err);
      }
    );
    return q.promise;
  };

  return { getAllItems: getAllItems };
]}); // <-- factory

Initially I was returning the factory straight away. The controller did getAllItems() which ran before the data was ready. The view was initially empty, only showing anything on return to the route after a second getAllItems()

So I tried delaying the return of the factory by adding a factoryReady() function and only calling it once all the internal DB stuff was ready

var factoryReady = function(){
  return {
    getAllItems: getAllItems
  };
};

And now there’s an undefined error as the entire factory is unavailable when first called, rather than getAllItems() simply returning empty-handed. I can see that the SQL database is being correctly written to in due course, but Angular throws an exception before this has finished.

I realise now that this is predictable, I’ve read the post AngularJS : Initialize service with asynchronous data but don’t quite understand how to implement the top-ranked answer (by joakimbl)

What’s the best way to expose the service and ensure it’s not called by the controller until the internal async stuff has finished? Do I need to return the ENTIRE service as a promise rather than just the result from getAllItems? I had a go at this but am now confused. Thanks.

EDIT

I’ve also looked into using ui-router’s resolve when loading the view http://blog.brunoscopelliti.com/show-route-only-after-all-promises-are-resolved but that doesn’t fix the internal readiness of the SQL data / factory. If I return the getAllCases method then it is still immediately called, there is nothing in the SQL database yet, the SQL query returns an empty results set, the promise resolves and the view is rendered.

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

Managed to get it working in the end. Posting this here for anyone else having the issue.

dataFactory.js

  • Reworked all private methods using async SQL calls in dataFactory.js to return promises
  • Created a public initDB method which chained calls to the private methods (e.g. openDB >> dropTable_ >> createTable_ etc). Also returned a promise (empty)
  • Returned initDB and getAllItems() from the factory immediately

    .factory('dataFactory', [function($window, $log, $q, $cordovaSQLite, dummyDataGenerator){    
    
      var db_;
    
      // private methods - all return promises
    
      var openDB_ = function(dbName){
    
        var q = $q.defer();
        // ...call async SQL methods
        return q.promise;
      };
    
      var createTable_ = function(){
        var q = $q.defer();
        // ...call async SQL methods
        return q.promise;               
      };
    
      // ...etc
    
      // public methods
    
      var initDB = function(){
    
        var q = $q.defer();
        // successively call private methods, chaining to next with .then()
        openDB_("myDB").then(function(db){
          var schema = "...SQL schema here..."
          dropTable_(db, "FirstTable", schema).then(function(tableName){
            // ...etc
            // when all done, resolve the promise
            q.resolve();
          })
        })
        return q.promise;
      }
    
      var getAllItems = function(){
    
        var q = $q.defer();
        // ...call async SQL methods
        return q.promise;
      };
    
      return {
        initDB: initDB,
        getAllItems: getAllItems 
      };
    
    ]}); // <-- factory
    

app.js

  • Used the resolve ability of ui-router
  • My previous attempts had not correctly injected promises
  • Added a resolve to the top-level abstract state to fire off the call to initDB
  • Injected the promise from initDB to the child state’s resolve object
  • Inject the resolve object into the controller

    // APP ROUTING (using ui-router)
    .config(function($stateProvider, $urlRouterProvider){

    $stateProvider
    
      // top-level abstract state that houses Ionic side menu & nav
      .state('app', {
        url: '/app',
        abstract: true,
        templateUrl: "templates/sideMenu.html",
        resolve: {
          dbReady: function($log, dataFactory){
            // (1) init the DB
            return dataFactory.initDB().then(function(){
              $log.log("initDB promise resolved");
          });
        }
      }
    })
    
    // the following states are all child states of app
    
    .state('app.items', {
      url: "/items",
      views: {
        menuContent: {
          templateUrl: "templates/gbCaseList.html",
          // (3) now we can inject the items promise into our controller
          controller: function($scope, $log, items){
            // (4) uses resolved items variable injected by ui-router
            $scope.allItems = items;
          }
        }
      },
      resolve: {
        // (2) note that we MUST inject the dbReady promise, if we don't this will instantiate immediately
        items: function(dbReady, $log, dataFactory){
          // the following call returns a promise
          return dataFactory.getItems();
        }
      }
    })
    

All working now. Massive thanks to this post for clearing up my use of ui-router Run controllers only after initialization is complete in AngularJS

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply