Displaying Local Notifications in Ionic


In this video we are taking a look at the final step in our Commangular command sequence. By the end of this step, the chosen TweetHours will be passed in to the $cordovaLocalNotification service, which will then ensure our notifications display correctly on the user's mobile device.

This step builds on the steps described in the previous two videos so if jumping in and unsure about what's going on, be sure to check those videos out first.

Preparing to Update Local Notifications

As discussed in the previous two steps, the easiest way to get the $cordovaLocalNotification to do what we want:

To schedule our notifications!

... is to pass in the data it expects in a format it can work with.

This sounds bleeding obvious. But, as software developers, and without a solid plan to follow, it's very easy to fall in to the trap of making your methods do too much.

Part of the reason for using Commangular in this work flow has been to mitigate this very problem.

Being able to split up one long process into three smaller tasks has - in some ways - made the code easier to reason about, understand, and test.

However, splitting the code up in to three steps has made the code harder to understand for someone brand new to the project. That's my personal opinion.

In this last step, testing is the hardest part of all. I will come to this again shortly.

What's Happening Here

Much like in the previous steps, our Commangular command is taking the result from the previous step, in this case the result will be :

PrepareHoursForNotificationServiceCommand_result

And, again, because of the way Commangular works, we will receive this data as the last param in our function call:

'use strict';

angular.module('core');

    commangular.create('UpdateNotificationScheduleCommand', [
        'NotificationUpdater',
        'PrepareHoursForNotificationServiceCommand_result',
        function(NotificationUpdater, PrepareHoursForNotificationServiceCommand_result) {
            return {
                execute: function() {
                    NotificationUpdater.updateNotifications(PrepareHoursForNotificationServiceCommand_result);
                }
            }
        }
    ])
;

The Commangular command isn't doing the real work. It delegates to the NotificationUpdater service, and passes in the result from our previous command step (PrepareHoursForNotificationServiceCommand_result).

Whilst not directly evident at this point, the NotificationUpdater.updateNotifications() method will interact with the mobile device that the app is running on. It relies on being able to talk to the device. And that makes testing this step really, really tricky.

Updating Notifications using $cordovaLocalNotification

I've stripped out a few lines of logging here, but the rest is as-is.

Let's take a look, then review:

// www/app/modules/core/services/notification-updater.service.js
'use strict';

angular.module('core')

    .service('NotificationUpdater', [
        '$ionicPlatform',
        '$cordovaLocalNotification',
        '$cordovaDevice',
        function($ionicPlatform, $cordovaLocalNotification, $cordovaDevice) {

        this.updateNotifications = function (notifications) {

            if (_.isEmpty(notifications)) {
                return false;
            }

            $ionicPlatform.ready(function () {

                $cordovaLocalNotification.cancelAll();

                var itemsToSchedule = [];

                _.each(notifications, function(tweetHour, timestampKey) {
                    timestampKey = parseInt(timestampKey);

                    itemsToSchedule.push({
                        id: timestampKey,
                        title: "TweetHours are Starting",
                        text: tweetHour.message,
                        at: new Date(moment(timestampKey,'X').toISOString()),
                        badge: tweetHour.badges,
                        every: 'week'
                    });
                });

                $cordovaLocalNotification.schedule(itemsToSchedule);

            });

            return true;
        };
    }])
;

As mentioned, this is where we actually talk to the mobile device. That's why we need all the Ionic services that interact with the underlying handset:

function($ionicPlatform, $cordovaLocalNotification, $cordovaDevice)

Then, we have our method defined, which expects to receive some notifications:

this.updateNotifications = function (notifications)

I'm using the Underscore library to check if the passed in notifications are empty. This is a follow on from the previous step.

if (_.isEmpty(notifications)) {
    return false;
}

If we have no notifications then simply don't do anything, and return early so as not to waste any further time / CPU power.

Device Ready

Everything from this point on relies on the mobile device that is running our app to be available:

$ionicPlatform.ready(function () {

As you can likely imagine, this makes testing... tricky.

And by tricky, I mean difficult enough to the point that I ended up doing this manually. Arghhh, admitting defeat is hard.

There came a point during development where I had to get TweetHours shipped. I couldn't spend any further time on the tests as there was a very good chance that I would be launching to crickets and would never recoup the time invested.

Erase And Rewind

Having spent many an hour trying to get the Local Notifications working, I had come to the conclusion that rather than risk any possibilty of left over or duplicate notifications from displaying, the simplest solution appeared to be cancelling every notification and re-adding them every time a new TweetHour was selected.

This might seem like overkill, but given that I have a full list of every hour, and that the total array size is really small (<300 TweetHours maximum), it really was a microscopic amount of overhead.

Therefore, we start by cancelling all scheduled TweetHours:

$cordovaLocalNotification.cancelAll();

Confession Time

I spent an inordinately large amount of time trying to get the chosen TweetHours to work as required with the notification service.

Partly this is due to being unable to properly test.

But partly (a much larger part as it happens) was due to not Reading The Flipping Manual.

Oops.

There's a small section right here:

Note Scheduling multiple notifications at once only works reliable when calling schedule([]) with an array of all notifications.

That cost me a couple of days.

Yikes.

In one of the earlier iterations, rather than passing in all notifications in one go, I was looping through each notification and passing it to the schedule method.

As it happens, the schedule method can take either a single object or an array of objects:

$cordovaLocalNotification.schedule(itemsToSchedule);

But as noted in the docs, passing them in one by one does not work reliably. This manifest itself as a really odd bug where only some of the notifications would pop up on the device. If I had any hair, it would certainly have been torn out in frustration at this point.

So, erm, yeah... RTFM :/

One ID, But Multiple Hours?

One last point of potential confusion is this section:

_.each(notifications, function(tweetHour, timestampKey) {
    timestampKey = parseInt(timestampKey);

    itemsToSchedule.push({
        id: timestampKey,
        title: "TweetHours are Starting",
        text: tweetHour.message,
        at: new Date(moment(timestampKey,'X').toISOString()),
        badge: tweetHour.badges,
        every: 'week'
    });
});

This looks as though you may hit upon a bug where if two hours start at the same time, they would have the same ID.

However, during the previous steps, any TweetHours occuring on the same hour - e.g. if two TweetHours both began at 9am on a Monday - then they will be joined together.

This flows back to how I mentioned about ensuring that the data passed in is already nicely formatted for the Notification Service. At this stage, all we want to worry about is getting those hours in to the scheduler, not also having to format them so they behave nicely.

With this complete, we can simply pass in the resulting array of nicely formatted hours to our Cordova Local Notification Scheduler, and all works as expected.

It's hard to make an automated test for this as $ionicPlatform.ready(function () { would consistently break the tests.

Fortunately, because the other steps are tested, it keeps the potential cause of problems down to only a very small amount of code. Code that did give me many a headache, but it did help narrow down where to look in the manual ;)

Guts And Glory

With this step done, we don't need to pass any result on as there is no further command in our sequence.

We can simply return true and be happy.

At this point, our hours will pop up and display nicely on our device. Any device.

And that's the beauty of Ionic.

Episodes