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.
Archived Comments
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?
Maybe I am not an android guy, but I am not a fan of top tabs.
@Justin: I started a thread on Ionic's forum: http://forum.ionicframework...
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.
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
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.
I put it here: www.raymondcamden.com/enclo...
FYI, I'm new to the routing stuff as well.
Load time: If I remember, barely nothing.
Thanks! I got it working now!
Your link was messed up, I think due to markdown formatting. The link is here: [http://www.raymondcamden.co...](http://www.raymondcamden.co...
I don't use Markdown here, but I should. :)
Found this elegant solution, and for me it works like a charm:
http://forum.ionicframework...
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.
Hey Reymond,
Thanks for the useful article but using a dump page for overriding the async operation causes another issue on Android. Therefor it is a page, it is being recorded in history and hardware back button allows users to go back to that empty dash page. Did you find a formula for this issue already?
Hmm. What if the controller for the temp page auto-forwarded back to your 'proper' home page?
Thanks a lot..It worked. I have a question and that is can you please share me a link from which I can understand the exact functionality of $apply().
I'd check the AngularJS docs.
Saved my day!
I slightly modified the code:
I added:
$state.go('app.loading');
as soon .run() is executed
And then:
$state.go('app.index'); // homepage
at the end of ionic.ready
I had a big delay because using lots of plugins!!!
With ripple emulator all the JS calls are emulated and "in sync", so i noticed this
only when testing on android emulator on a slow PC.
Glad it was helpful.
Hey Raymond,
Are you still using this workaround ? Or figured a better way of delaying first view loading until deviceReady?
I really like your posts, always straight to the point! Thanks.
Yes and no. Keep in mind I mainly build stupid demos, so I don't always do things "The Best Way". In recent demos, I've taken to simply doing $ionicPlatform.ready() in the controller. I don't like that as a solution for a 'real' app, but it feels really simple for a demo, especially if I just have one controller, know what I mean?
Hi, Do you know that how can I load an ngCordova plugin later (not when the app is ready). I have a splash screen and after a login screen, and I would like to load the cordovaTouchID plugin. Unfortunately the plugin loads while the splash screen is still running.
I don't know if this is possible. Right now when you add a plugin, the JS behind that plugin is automatically included.
I'd ask why you care though? Why does the loading of the plugin matter?
Maybe just my logic is wrong, but I want to implement a login screen with an optional TouchID login function. At the moment the TouchID prompt appears, when I load the app (splash screen)
Yes, your logic is wrong. :) I can't tell you how to fix it because I don't know what plugin you are using, but you should be able to enable it on touch or some other event.
It's ngcordova (cordovaTouchID), and I have got the function, but not on an actual page.
Well again, it should work. I'd maybe file an issue on their page with your problem and see if they can help.
Ok, thanks.
Thanks for posting this solution. I'm new to Cordova and Ionic, so I was struggling to figure out why my screen kept crashing. I didn't realize that $scope was trying to access values within a plugin before the plugins were ready. I've got a loading screen now, while the device loads everything and then have the main screen showing when everything's ready.
Great stuff. Thanks again.
Any issues with just putting an ng-if="deviceReady" on a top level element (Then obviously putting a $rootscope.deviceReady = true)? I guess you want to try and do everything as async as possible and that blocks any UI rendering until deviceReady...
I've actually done this on more recent apps. So for example, if a button can't/shouldn't be used, I disable it till some scope var is true, and that scope var is set true when $ionicPlatform.ready is fired. I don't use the rootScope, but just the same code in my controller.
To me - that makes sense for a simpler app, but obviously if you had multiple buttons, I don't think it scales. In that case I'd still use something like I've done here - an intermediate view.
It really works! When you have services that use Cordova plugins, this solution is perfect! I It took a little long to detect the problem, since only some devices had a slow init time. My code worked 90% of the time, now works 100%.
Glad to know this old post still helps people. :)
I think you can delete the $urlRouterProvider.otherwise('/home'); Then in the run function, try ionic.Platform.ready, and then go to the intended state
I think the otherwise would probably only be useful in a web app since the URL could be mucked with, so you're probably right.
Thanks Ladna and Raymond. This issue was driving me nuts.
Hi Raymond,
You mean to say the Oder of operations would be
1) home
2) device ready
3) then move to tab dash
Looks like a good approach. Perhaps the home page in that case can also be a splash, with some setup logic in the background.
I'd like to this out.
Does this mean we can get rid of Ng-cloak as long as the home page doesn't have any angular in it.
Um - maybe? :) To be honest this post is so old I'm not sure how much it applies today. Nowadays I also tend to just use the ionicReady event in my controller as opposed to a 2 step page load.
It happened that I solved my issue by using the same trick I learnt from somewhere else, an intermediate loading view.Handling all your device ready events on this view. For those people who is still struggling with getting data from asynchronous APIs for your real function page(for authentication,for instance), or need data from your Cordova plugins, use ui router resolve on the route of this intermediate view(inject those API calls services using dependency injection) do a $state.go to your real functional page after all your data is ready. You can use the ui router otherwise statement to set the intermediate view as your default route. I was so desperate before I could find my solution. Just want to share it with the people who need it. Manually bootstrapping the app only works in some simple use cases. The new ui router solved this issue by introducing an asynchronous feature. It's a bit less elegant because we have to use this extra view but it's the best way I have found so far to solve this challenge. Just want to thank those people like Raymond who are willing to share their skills and experience with others.