Twitter: raymondcamden


Address: Lafayette, LA, USA

Ionic and Cordova's DeviceReady - My Solution

08-16-2014 7,612 views Mobile, JavaScript 11 Comments

Folks know that I've been madly in love with the Ionic framework lately, but I've run into an issue that I'm having difficulty with. I thought I'd blog about the problem and demonstrate a solution that worked for me. To be clear, I think my solution is probably wrong. It works, but it just doesn't feel right. I'm specifically sharing this blog entry as a way to start the discussion and get some feedback. On the slim chance that what I'm showing is the best solution... um... yes... I planned that. I'm brilliant.

The Problem

So let's begin by discussing the problem. Given a typical Ionic app, your Angular code will have a .run method that listens for the ionicPlatform's ready event. Here is an example from the "base" starter app (https://github.com/driftyco/ionic-app-base/blob/master/www/js/app.js):

// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

.run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      // Set the statusbar to use the default style, tweak this to
      // remove the status bar on iOS or change it to use white instead of dark colors.
      StatusBar.styleDefault();
    }
  });
})

The ionicPlatform.ready event is called when Cordova's deviceready event fires. When run on a desktop, it is fired when on window.load. Ok, so in my mind, this is where I'd put code that's normally in a document.ready block. So far so good.

Now let's imagine you want to use a plugin, perhaps the Device plugin. Imagine you want to simply copy a value to $scope so you can display it in a view. If that controller/view is the first view in your application, you end up with a race condition. Angular is going to display your view and fire off ionicPlatform.ready asynchronously. That isn't a bug of course, but it raises the question. If you want to make use of Cordova plugin features, and your application depends on it immediately, how do you handle that easily?

One way would be to remove ng-app from the DOM and bootstrap Angular yourself. I've done that... once before and I see how it makes sense. But I didn't want to use that solution this time as I wanted to keep using ionicPlatform.ready. I assumed (and I could be wrong!) that I couldn't keep that and remove the ng-app bootstraping.

So what I did was to add an intermediate view to my application. A simple landing page. I modified the stateProvider to add a new state and then made it the default. In my ionicPlatform.ready, I use the location service to do a move to the previously default state.

.run(function($ionicPlatform,$location,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }

	  $location.path('/tab/dash');
	  $rootScope.$apply();
  });
})

This seemed to do the trick. My controller code that's run for the views after this was able to use Cordova plugins just fine. How about a real example?

The Demo

One of the more recent features to land in Ionic is striped-style tabs. This is an Android-style tab UI and it will be applied automatically to apps running on Android. The difference is a bit subtle when the tabs are on the bottom:

But when moved to the top using tabs-top, it is a bit more dramatic.

Ok... cool. But I wondered - how can I get tabs on top just for Android? While I'm not one of those people who believe that UI elements have to be in a certain position on iOS versus Android, I was curious as to how I'd handle this programmatically.

Knowing that it was trivial to check the Device plugin, and having a way now to delay the view until my plugins were loaded, I decided to use the approach described above to ensure I could access the platform before that particular view loaded.

Here is the app.js file I used, modified from the tabs starter template.

// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
// 'starter.services' is found in services.js
// 'starter.controllers' is found in controllers.js
angular.module('starter', ['ionic', 'starter.controllers', 'starter.services'])

.run(function($ionicPlatform,$location,$rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }

	  $location.path('/tab/dash');
	  $rootScope.$apply();
  });
})

.config(function($stateProvider, $urlRouterProvider) {

  // Ionic uses AngularUI Router which uses the concept of states
  // Learn more here: https://github.com/angular-ui/ui-router
  // Set up the various states which the app can be in.
  // Each state's controller can be found in controllers.js
  $stateProvider

    // setup an abstract state for the tabs directive
  	.state('home', {
		url:"/home",
		templateUrl:'templates/loading.html',
		controller:'HomeCtrl'
	})
    .state('tab', {
      url: "/tab",
      abstract: true,
      templateUrl: function() {
		  if(window.device.platform.toLowerCase().indexOf("android") >= 0) {
			  return "templates/tabs_android.html";			  
		  } else {
			  return "templates/tabs.html";
		  }
	  },
    })

    // Each tab has its own nav history stack:

    .state('tab.dash', {
      url: '/dash',
      views: {
        'tab-dash': {
          templateUrl: 'templates/tab-dash.html',
          controller: 'DashCtrl'
        }
      }
    })

    .state('tab.friends', {
      url: '/friends',
      views: {
        'tab-friends': {
          templateUrl: 'templates/tab-friends.html',
          controller: 'FriendsCtrl'
        }
      }
    })
    .state('tab.friend-detail', {
      url: '/friend/:friendId',
      views: {
        'tab-friends': {
          templateUrl: 'templates/friend-detail.html',
          controller: 'FriendDetailCtrl'
        }
      }
    })

    .state('tab.account', {
      url: '/account',
      views: {
        'tab-account': {
          templateUrl: 'templates/tab-account.html',
          controller: 'AccountCtrl'
        }
      }
    });

  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/home');

});

You can see where I use the location.path mechanism after the ionicPlatform.ready event has fired. You can also see where I sniff the device platform to determine which template to run. tabs_android.html is the exact same as tabs.html - but with the tabs-top class applied (*). The biggest drawback here is that the application would error when run on the desktop. That could be avoided by sniffing for the lack of window.device and just setting it to some default: window.device = {platform : "ios"};

So that's it. What do you think? I have to imagine there is a nicer way of handling this. Maybe I'm being lazy but I want to use Ionic's killer directives and UX stuff along with Cordova plugins and not have to use an awkward workaround like this.

* A quick footnote. I noticed that if I tried to add tabs-top to the ion-tabs directive, it never worked. For example, this is what I tried first: <ion-tabs ng-class="{'tabs-top':settings.isAndroid}"> I used code in my controller that always set it to true (I wasn't worried about the device plugin yet) and it never actually updated the view. It's like the controller scope couldn't modify the view for some odd reason.

11 Comments

  • Commented on 08-18-2014 at 7:57 AM
    I saw your discussion of this on Twitter. Did you every get a resolution to Max indicated this needs to be fixed? I know Perry mentioned it, but I didn't seem follow-ups.

    Care to update this blog with any new info?
  • Commented on 08-18-2014 at 9:22 AM
    Maybe I am not an android guy, but I am not a fan of top tabs.
  • Commented on 08-18-2014 at 9:40 AM
    @Justin: I started a thread on Ionic's forum: http://forum.ionicframework.com/t/open-discussion-...
  • Tyler Collier #
    Commented on 09-12-2014 at 12:55 AM
    Would you be willing to show screenshots of the flow? So how long does your loading page typically show? Is it even noticeable?

    I'm not sure if mine's working. Then again I'm using ionic serve and the phonegap developer app which I think essentially fire the ready immediately since there they don't have cordova components.
  • Tyler Collier #
    Commented on 09-12-2014 at 1:07 AM
    Actually it'd be great, if possible, if you could share your entire example (perhaps a link to a github project). You're using tabs, which is not part of the "base" starter app. Specifically I'm wondering what your index.html looks like.

    PS: You are a developer for Adobe? Where's the love? Can you not just walk over to somebody's desk there and say hey? :-D
  • Tyler Collier #
    Commented on 09-12-2014 at 1:41 AM
    I figured out my problem. It was with how I set up my states in ui-router (I was copying one of the tabs like tab.dash, which wasn't a good one to copy for this non subview!). Unfortunately I am a rookie with ui-router and didn't have any console log messages to help.

    I'm still curious about your load time. I'm seeing it load more or less instantaneously, but then again my list of plugins is small since I'm basically just starting my app.
  • Commented on 09-12-2014 at 6:18 AM
    I put it here: www.raymondcamden.com/enclosures/www_sep12_2014.zi...

    FYI, I'm new to the routing stuff as well.

    Load time: If I remember, barely nothing.
  • Tyler Collier #
    Commented on 09-17-2014 at 12:07 PM
    Thanks! I got it working now!

    Your link was messed up, I think due to markdown formatting. The link is here: [http://www.raymondcamden.com/enclosures/www_sep12_...](http://www.raymondcamden.com/enclosures/www_sep12_...)
  • Commented on 09-17-2014 at 12:24 PM
    I don't use Markdown here, but I should. :)
  • Stefan Göbel #
    Commented on 10-28-2014 at 2:33 PM
    Found this elegant solution, and for me it works like a charm:

    http://forum.ionicframework.com/t/sqlite-plugin-an...
  • Commented on 10-28-2014 at 2:55 PM
    I believe I mentioned this as an option and specifically did not want to use it. I didn't have a great reason I just - didn't want to. But it is valid for sure.

Post Reply

Please refrain from posting large blocks of code as a comment. Use Pastebin or Gists instead. Text wrapped in asterisks (*) will be bold and text wrapped in underscores (_) will be italicized.

Leave this field empty