🎉 New release for Pusher Chatkit - Webhooks! Extend your in-app chat functionality
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Sending push notifications from Dart to Android and iOS

  • Suragch
June 10th, 2019
You will need Dart, Aqueduct and either Android Studio or XCode installed on your machine.

Introduction

In today's tutorial we will learn how to send push notifications from a Dart server to Android and iOS using Pusher Beams. In order to trigger events in the server, we'll make a simple admin user app. The final result will look like this:

When the admin user presses a button, the admin app contacts the Dart server. The Dart server then tells Pusher to send a push notification to users. The first button notifies all users who have subscribed to a Device Interest. The second button notifies a single authenticated user.

Prerequisites

I'm assuming you have some familiarity with Server Side Dart, but if not, no problem. Check out these tutorials first:

Required software:

If you don't want to make both an Android app and an iOS app at this time, you can choose just one. I’m assuming you are already familiar with mobile app development.

This tutorial was made using Dart 2.2, Android Studio 3.4, and Xcode 10.2.

Pusher Beams overview

Beams is a service from Pusher to simplify sending push notifications to users. It allows you to send notifications to groups of users who have all subscribed to a certain topic, which Pusher calls Device Interests. You can also send private push notifications to individual users.

Let me describe what we will be doing today.

Interests

The user client app (Android or iOS) tells Pusher that they are interested in some topic. Lets say it's apples. Pusher makes note of it.

The app admin wants to tell everyone who is interested in apples that they are on sale. So the admin tells the Dart server. Dart doesn't know who is interested in apples, but Pusher does, so Dart tells Pusher. Finally, Pusher sends a push notification to every user that is interested in apples.

Authenticated users

Sometimes a user (let's call her Mary) may want to get personalized notifications. To do that she has to prove who she is by authenticating her username and password on the Dart server. In return, the Dart server gives her a token.

Mary gives the token to Pusher, and since the token proves she really is Mary, Pusher makes a note of it.

Now Mary ordered some apples a couple days ago, and the company admin wants to let Mary know that they will be arriving today. The admin tells the Dart server, the server tells Pusher, and Pusher tells Mary. Pusher doesn't tell any other user, just Mary.

Implementation plan

So that is how device interests and user push notifications work. In order to implement this we have four tasks to do. We have to create an Android user app, an iOS user app, an admin app, and the Dart server. Feel free to make only one user app rather than both the Android and the iOS one, though.

Android user app

Create a new Android app. (I'm using Kotlin and calling my package name com.example.beamstutorial.) Then set it up as described in the Pusher Beams documentation. Make sure that you can receive the basic "Hello world" notification. I won't repeat those directions here, but there are a couple of things to note:

  • If you notice a version conflict error with the appcompat support in build.gradle, see this solution.
  • Your app should be minimized in order to receive push notifications.

Now you should have a Pusher Beams account, a Firebase account, and an Android app that receives push notifications.

Interest notifications

Let's modify the app to tell Pusher that the user is interested in "apples" instead of "hello".

In the MainActivity, update the following line:

    PushNotifications.addDeviceInterest("apples")

That's all we need to do to tell Pusher that this user is interested in apples.

Authenticated user notifications

In order to receive personalized messages from Pusher we need to get a token from the Dart server (which we will be building soon).

We’re going to add an AsyncTask to make the request. Update the MainActivity to look like the following:

    // app/src/main/java/com/example/beamstutorial/MainActivity.kt

    package com.example.beamstutorial

    import android.os.AsyncTask
    import android.support.v7.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Base64
    import android.util.Log
    import com.pusher.pushnotifications.BeamsCallback
    import com.pusher.pushnotifications.PushNotifications
    import com.pusher.pushnotifications.PusherCallbackError
    import com.pusher.pushnotifications.auth.AuthData
    import com.pusher.pushnotifications.auth.AuthDataGetter
    import com.pusher.pushnotifications.auth.BeamsTokenProvider

    class MainActivity : AppCompatActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)

            // TODO replace this instance ID with your own
            val instanceId = "your_instance_id_here"
            PushNotifications.start(getApplicationContext(), instanceId)

            // Interests
            PushNotifications.addDeviceInterest("apples")

            // Authenticated user
            RegisterMeWithPusher().execute()
        }

        class RegisterMeWithPusher : AsyncTask<Void, Void, Void>() {
            override fun doInBackground(vararg params: Void): Void? {

                // hardcoding the username and password both here and on the server
                val username = "Mary"
                val password = "mypassword"
                val text = "$username:$password"
                val data = text.toByteArray()
                val base64 = Base64.encodeToString(data, Base64.NO_WRAP)

                // get the token from the Dart server
                val serverUrl = "http://10.0.2.2:8888/token"
                val tokenProvider = BeamsTokenProvider(
                    serverUrl,
                    object: AuthDataGetter {
                        override fun getAuthData(): AuthData {
                            return AuthData(
                                headers = hashMapOf(
                                    "Authorization" to "Basic $base64"
                                )
                            )
                        }
                    }
                )

                // send the token to Pusher
                PushNotifications.setUserId(
                    username,
                    tokenProvider,
                    object : BeamsCallback<Void, PusherCallbackError> {
                        override fun onFailure(error: PusherCallbackError) {
                            Log.e("BeamsAuth", 
                                  "Could not login to Beams: ${error.message}")
                        }
                        override fun onSuccess(vararg values: Void) {
                            Log.i("BeamsAuth", "Beams login success")
                        }
                    }
                )

                return null
            }
        }
    }

Don’t forget to replace the instance ID with your own (from the Beams dashboard).

In the manifest add the INTERNET permission:

    <uses-permission android:name="android.permission.INTERNET" />

And since we are using a clear text HTTP connection in this tutorial, we need to also update the manifest to include the following:

    <application
        android:usesCleartextTraffic="true"
        ...
        >

You should not include this in a production app. Instead, use an HTTPS connection (see this tutorial for more explanation).

That's all we can do for now until the server is running. You can skip the iOS user app section if you like and go on to the admin app section.

iOS user app

Create a new iOS app. (I'm calling my package name com.example.beamstutorial.) Then set it up as described in the Pusher Beams documentation. Make sure that you can receive the basic "Hello world" notification. I won't repeat those directions here, but there are a couple of things to note:

  • At the time of this writing, the Cocoa Pods installation had a deprecated API for the Beams PushNotifications. The Carthage installation is up-to-date, though. See this tutorial for help with Carthage.
  • You need to add "remote-notification" to the list of your supported UIBackgroundModes in your Info.plist file. Rather than editing it directly, you can make the change in Targets > your app Capabilities > Background Modes (see this for help).
  • You can't receive push notifications in the iOS simulator. You have to use a real device.
  • Your app should be minimized in order to receive the push notifications.

Now you should have a Pusher Beams account, APNs configured, and an iOS app installed on a real device that receives push notifications.

Interest notifications

Let's modify the app to tell Pusher that the user is interested in "apples" instead of "hello". In AppDelegate.swift, update the following line:

    try? self.pushNotifications.addDeviceInterest(interest: "apples")

That's all we need to do to tell Pusher that this user is interested in apples.

Authenticated user notifications

In order to receive personalized messages from Pusher, we need to get a token from the Dart server (which we will be making soon).

Open ViewController.swift and replace it with the following code:

    // beamstutorial/ViewController.swift

    import UIKit
    import PushNotifications

    class ViewController: UIViewController {

        let beamsClient = PushNotifications.shared

        // hardcoding the username and password both here and on the server
        let userId = "Mary"
        let password = "mypassword"

        // TODO: As long as your iOS device and development machine are on the same wifi 
        // network, change the following IP to the wifi router IP address where your 
        // Dart server will be running.
        let serverIP = "192.168.1.3"

        override func viewDidLoad() {
            super.viewDidLoad()

            // get the token from the server
            let serverUrl = "http://\(serverIP):8888/token"
            let tokenProvider = BeamsTokenProvider(authURL: serverUrl) { () -> AuthData in
                let headers = ["Authorization": self.authHeaderValueForMary()]
                let queryParams: [String: String] = [:]
                return AuthData(headers: headers, queryParams: queryParams)
            }

            // send the token to Pusher
            self.beamsClient.setUserId(userId,
                                       tokenProvider: tokenProvider,
                                       completion:{ error in
                guard error == nil else {
                    print(error.debugDescription)
                    return
                }
                print("Successfully authenticated with Pusher Beams")
            })
        }

        func authHeaderValueForMary() -> String {
            guard let data = "\(userId):\(password)".data(using: String.Encoding.utf8)
                else { return "" }
            let base64 = data.base64EncodedString()
            return "Basic \(base64)"
        }
    }

Since we are using a clear text HTTP connection in this tutorial, we need to also update the Info.plist file to include the following:

    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>

You should not include this in a production app. Instead, use an HTTPS connection (see this tutorial for more explanation).

It's extra tricky getting a real device talking to the Dart server that will be running on our development machine. We can't just connect to localhost like we can when we use the iOS simulator. As I noted in the comments above, if your iPhone and development machine are sharing a wifi network, you can use the router IP address of your development machine. So if you haven't already, update that in the code above. For help finding the IP address, check out the following links:

That's all we can do for now until the server is running.

Admin app

We need a means to trigger the Dart server to talk to Pusher and send the push notifications. I'm going to make a simple iOS app running on the simulator, but you can do it another way if you like. You could user curl, Postman, or an Android app. If you choose one of those, this is the REST API you will need to set up.

    // send push notification to interests
    POST http://localhost:8888/admin/interests

    // send push notification to user
    POST http://localhost:8888/admin/users

The authorization header for both of these should be Basic with a username of admin and the password as password123. Base64 encoded, this would be:

    Authorization: Basic YWRtaW46cGFzc3dvcmQxMjM=

iOS version of the admin app

In a new Xcode project, create a simple layout with two buttons:

Note: This admin app does not send the actual device interest or username in the body of the HTTP request. The reason for that was to keep this tutorial as short as possible by avoiding the need to serialize and deserialize JSON. The push notifications are hardcoded on the server. Simply sending a POST request to the proper route will make the server send the push notifications.

Replace ViewController.swift with the following code:

    // beams_admin_app/ViewController.swift

    import UIKit

    class ViewController: UIViewController {

        // hardcoding the username and password both here and on the Dart server
        let username = "admin"
        let password = "password123"

        // using localhost is ok since this app will be running on the simulator
        let host = "http://localhost:8888"

        // tell server to send a notification to device interests
        @IBAction func onInterestsButtonTapped(_ sender: UIButton) {

            // set up request
            guard let url  = URL(string: "\(host)/admin/interests") else {return}
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue(authHeaderValue(), forHTTPHeaderField: "Authorization")

            // send request
            let task = URLSession.shared.dataTask(with: request) {
                (data, response, error) in
                guard let statusCode = (response as? HTTPURLResponse)?.statusCode
                    else {return}
                guard let body = data
                    else {return}
                guard let responseString = String(data: body, encoding: .utf8)
                    else {return}

                print("POST result: \(statusCode) \(responseString)")
            }
            task.resume()
        }

        // Returns the Auth header value for Basic authentication with the username
        // and password encoded with Base64. In a real app these values would be obtained
        // from user input.
        func authHeaderValue() -> String {
            guard let data = "\(username):\(password)".data(using: .utf8) else {
                return ""
            }
            let base64 = data.base64EncodedString()
            return "Basic \(base64)" // "Basic YWRtaW46cGFzc3dvcmQxMjM="
        }

        // tell server to send notification to authenticated user
        @IBAction func onUserButtonTapped(_ sender: UIButton) {

            // set up request
            guard let url  = URL(string: "\(host)/admin/users") else {return}
            var request = URLRequest(url: url)
            request.httpMethod = "POST"
            request.setValue(authHeaderValue(), forHTTPHeaderField: "Authorization")

            // send request
            let task = URLSession.shared.dataTask(with: request) {
                (data, response, error) in
                guard let statusCode = (response as? HTTPURLResponse)?.statusCode
                    else {return}
                guard let body = data
                    else {return}
                guard let responseString = String(data: body, encoding: .utf8)
                    else {return}

                print("POST result: \(statusCode) \(responseString)")
            }
            task.resume()
        }
    }

Remember to hook up the buttons to the IBAction methods.

As before, since we are using a clear text HTTP connection in this tutorial, we need to also update the Info.plist file to include the following:

    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsArbitraryLoads</key>
        <true/>
    </dict>

That's it for now. Let's make the Dart server.

Dart server

Create a new Aqueduct server project:

    aqueduct create dart_server

Dependencies

At the time of this writing Pusher does not officially support Dart servers, so I created a Dart server SDK based on the API docs. It is available on Pub here. In your server's pubspec.yaml file, add the dependency:

    dependencies:
      pusher_beams_server: ^0.1.4

We'll set up three routes in channel.dart. Open that file and replace it with the following code.

    // dart_server/lib/channel.dart

    import 'package:dart_server/controllers/auth.dart';
    import 'package:dart_server/controllers/token.dart';
    import 'package:dart_server/controllers/interests.dart';
    import 'package:dart_server/controllers/users.dart';
    import 'dart_server.dart';

    class DartServerChannel extends ApplicationChannel {

      // These middleware validators will check the username 
      // and passwords before allowing them to go on. 
      BasicValidator normalUserValidator;
      AdminValidator adminValidator;

      @override
      Future prepare() async {
        logger.onRecord.listen(
            (rec) => print("$rec ${rec.error ?? ""} ${rec.stackTrace ?? ""}"));
        normalUserValidator = BasicValidator();
        adminValidator = AdminValidator();
      }

      @override
      Controller get entryPoint {
        final router = Router();

        // user app will get a Pusher auth token here
        router
            .route('/token')
            .link(() => Authorizer.basic(normalUserValidator))
            .link(() => TokenController());

        // admin app will send push notifications for device interests here
        router
            .route('/admin/interests')
            .link(() => Authorizer.basic(adminValidator))
            .link(() => InterestsController());

        // admin app will send push notifications to authenticated users here
        router
            .route('/admin/users')
            .link(() => Authorizer.basic(adminValidator))
            .link(() => UsersController());

        return router;
      }
    }

Create a controllers folder in lib. Make a file name auth.dart where we will put the username and password validation middleware. Paste in the following code:

    // dart_server/lib/controllers/auth.dart

    import 'dart:async';
    import 'package:aqueduct/aqueduct.dart';

    // Hardcoding username and passwords both here and in the client apps
    // admin: password123
    // Mary: mypassword
    // hash generated with AuthUtility.generatePasswordHash()
    // A production app would store these in a database.
    final Map<String, User> adminUsers = {
      'admin': User(
          username: 'admin',
          saltedPasswordHash: 'ntQLWWIu/nubfZhCEy9sXgwRijuBV+d9ZN2Id3hTLbs=',
          salt: 'mysalt1'),
    };
    final Map<String, User> normalUsers = {
      'Mary': User(
          username: 'Mary',
          saltedPasswordHash: 'JV0R5CH9mnA6rcOGnkzSvIeGkHUvtnnvUCuFBc3XD+4=',
          salt: 'mysalt2'),
    };

    class User {
      User({this.username, this.saltedPasswordHash, this.salt});
      String username;
      String saltedPasswordHash;
      String salt;
    }

    class BasicValidator implements AuthValidator {

      final _requireAdminPriveleges = false;

      @override
      FutureOr<Authorization> validate<T>(
          AuthorizationParser<T> parser, T authorizationData,
          {List<AuthScope> requiredScope}) {

        // Get the parsed username and password from the basic
        // authentication header.
        final credentials = authorizationData as AuthBasicCredentials;

        // check if user exists
        User user;
        if (_requireAdminPriveleges) {
          user = adminUsers[credentials.username];
        } else {
          user = normalUsers[credentials.username];
        }
        if (user == null) {
          return null;
        }

        // check if password matches
        final hash = AuthUtility.generatePasswordHash(credentials.password, user.salt);
        if (user.saltedPasswordHash == hash) {
          return Authorization(null, null, this, credentials: credentials);
        }

        // causes a 401 Unauthorized response
        return null;
      }

      // This is for OpenAPI documentation. Ignoring for now.
      @override
      List<APISecurityRequirement> documentRequirementsForAuthorizer(
          APIDocumentContext context, Authorizer authorizer,
          {List<AuthScope> scopes}) {
        return null;
      }
    }

    class AdminValidator extends BasicValidator {
      @override
      bool get _requireAdminPriveleges => true;
    }

Next make a file named tokens.dart (in controllers) to handle when the user app needs to get a Pusher Beams auth token so that it can receive personalized push notifications. Paste in the following code:

    // dart_server/lib/controllers/token.dart

    import 'dart:async';
    import 'package:aqueduct/aqueduct.dart';
    import 'package:pusher_beams_server/pusher_beams_server.dart';
    import 'package:dart_server/config.dart';

    class TokenController extends ResourceController {
      PushNotifications beamsClient;

      @Operation.get()
      Future<Response> generateBeamsTokenForUser() async {

        // get the username from the already authenticated credentials
        final username = request.authorization.credentials.username;

        // generate the token for the user
        beamsClient ??= PushNotifications(Properties.instanceId, Properties.secretKey);
        final token = beamsClient.generateToken(username);

        // return the token to the user
        return Response.ok({'token':token});
      }
    }

Also in the controllers folder, create a file called interests.dart. When requested by the admin app, it will tell Pusher to send notifications to users who have subscribed the apples interest. Paste in the following code:

    // dart_server/lib/controllers/interests.dart

    import 'dart:async';
    import 'dart:io';
    import 'package:aqueduct/aqueduct.dart';
    import 'package:pusher_beams_server/pusher_beams_server.dart';
    import 'package:dart_server/config.dart';

    class InterestsController extends ResourceController {
      PushNotifications beamsClient;

      // send push notifications to users who are subscribed to the interest
      @Operation.post()
      Future<Response> notifyInterestedUsers() async {
        beamsClient ??= PushNotifications(Properties.instanceId, Properties.secretKey);

        const title = 'Sale';
        const message = 'Apples are 50% off today!';

        final fcm = {
          'notification': {
            'title': title,
            'body': message,
          }
        };
        final apns = {
          'aps': {
            'alert': {
              'title': title,
              'body': message,
            }
          }
        };
        final response = await beamsClient.publishToInterests(
          ['apples'],
          apns: apns,
          fcm: fcm,
        );

        return Response.ok(response.body)..contentType = ContentType.text;
      }
    }

And again in the controllers folder, create a file called users.dart. When requested by the admin app, it will tell Pusher to send a personal notification the user Mary. Paste in the following code:

    // dart_server/lib/controllers/users.dart

    import 'dart:async';
    import 'dart:io';
    import 'package:aqueduct/aqueduct.dart';
    import 'package:pusher_beams_server/pusher_beams_server.dart';
    import 'package:dart_server/config.dart';

    class UsersController extends ResourceController {
      PushNotifications beamsClient;

      // send push notification to Mary
      @Operation.post()
      Future<Response> notifyAuthenticatedUsers() async {
        beamsClient ??= PushNotifications(Properties.instanceId, Properties.secretKey);

        const title = 'Purchase';
        const message = 'Hello, Mary. Your purchase of apples will be delivered shortly.';

        final apns = {
          'aps': {
            'alert': {
              'title': title,
              'body': message,
            }
          }
        };
        final fcm = {
          'notification': {
            'title': title,
            'body':
                message,
          }
        };
        final response = await beamsClient.publishToUsers(
          ['Mary'],
          apns: apns,
          fcm: fcm,
        );

        return Response.ok(response.body)..contentType = ContentType.text;
      }
    }

In order not to expose the secret key in GitHub, let’s put it in a configuration file and add that file to .gitignore. Create a file called config.dart in the lib folder.

    // dart_server/lib/config.dart

    // Include this file in .gitignore
    class Properties {
      // exchange these values with valid ones from your Beams dashboard
      static const instanceId = 'your_instance_id_here';
      static const secretKey = 'your_secret_key_here';
    }

Don't forget to add the filename to .gitignore and also to replace the instanceId and secretKey with your own (from the Beams dashboard).

Save all of your changes. We're finally ready to test everything out.

Testing

Set everything up as follows:

Aqueduct Dart server

Start the Aqueduct server in the terminal with the following command:

    aqueduct serve

Admin app

Start the admin app in the iOS simulator. (Or use Postman or curl or your own Android implementation. See the admin app section above for notes about that.)

User apps

Depending on what you made, prepare the Android or iOS user app (or both):

  • Install the Android user app on an Android emulator.
  • Install the iOS user app on a real device connected to the same wifi network as your Aqueduct server. Remember to update the router IP of the server in your app.

Minimize the user app so that it is in the background.

Putting it all together

  • Press the first button on the admin app to notify users interested in "apples". You should see a notification pop up on the user app.
  • Press the second button on the admin app to notify the user "Mary". You should see a notification pop up on the user app.

Hopefully it worked for you. If it didn’t check the logs, and make sure you remembered to do the following tasks:

  • You are using the correct instance ID in the iOS AppDelegate.swift, the Android MainActivity.kt, and the Dart config.dart.
  • You hooked up the buttons to the code in the iOS admin app.
  • You iOS user app is on the same wifi network as your Dart server and has the correct router IP for the Dart server.

Conclusion

Today we learned how to send push notifications from a Dart server to Android and iOS.

In our contrived example, we sent a message to a single user who was interested in "apples". If there had been a hundred or even a thousand users who were all interested in apples, the message would have gone out to all of them. Users can subscribe to different interests and you can use this to target your notification messages to appropriate groups of users.

We also sent a personalized message to an authenticated user. This ability allows us to send private updates to users for things that only apply to them.

You’ve got the technical know-how now. Now use your imagination to build something amazing! (Or at least don’t make something annoying . . . like an app that spams its users with push notifications 20 times a day. I borrowed my mom’s iPhone to make this tutorial and she was wondering why she keeps getting notifications about apples. And who’s Mary anyway? Oops.)

The source code for the projects in this tutorial are available on GitHub. Also check out the Pusher Beams Dart server SDK documentation.

Clone the project repository
  • Android
  • Beams
  • Dart
  • iOS
  • Kotlin
  • Swift
  • Beams

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.