Sending SMS Messages with Dart and Twilio Programmable SMS

March 13, 2020
Written by
Alex Baban
Contributor
Opinions expressed by Twilio contributors are their own

sending-sms-with-dart.png

According to GitHub Octoverse Dart was the fastest growing language of 2019.

Dart is a client-optimized language for fast apps on any platform. Developed by Google, Dart is used to build mobile, desktop, backend, and web applications. Dart code is easy to write and doesn't require lots of hassle with memory management and build systems.

Since version 2.6, announced November 2019, Dart comes with dart2native, a compiler with the ability to produce platform-specific, standalone executables. This enables developers to create tools for the command line on macOS, Windows, or Linux. The entirely self-contained executables can run instantly on machines that don’t need to have the Dart SDK installed.

Twilio’s Programmable SMS API makes sending and receiving SMS easy. You can send messages globally using Twilio phone numbers that handle carrier complexities for you. By wrapping the API in a package, you and your fellow Dart developers can get to the important work even faster.

What are you going to learn

Creating Dart packages is both easy and fun, and this post will show you how. You learn how to build a Dart library package and use it in a different package. You’ll learn how to call Twilio’s Programmable SMS API from Dart code. And you’ll learn how to convert your Dart package into an executable command-line application which takes arguments.

What are you going to build

Your own Dart package named my_twilio. You will then use my_twilio in a command line application to send text messages with Twilio’s API.

Prerequisites

Package layout conventions

A library package is a package that other packages can depend on. When you build a package there are some style conventions to follow. The ones most relevant to this project are:

  • name libraries, packages, directories, and source files using lowercase_with_underscores
  • name functions, variables, and parameters using lowerCamelCase
  • name classes and types using UpperCamelCase
  • place dart: imports at the top before other imports
  • place external package: imports before relative imports

Package layout conventions relevant to this project are:

  • every package has a pubspec.yaml file in the root directory of the package
  • libraries public to other packages should go inside the lib folder
  • implementation code is placed under lib/src and is considered private
  • if you include an example of how to use your library it should go into the example directory at the root of the package

The my_twilio package structure

my_twilio
 ├── example
 │    └── send_sms.dart
 ├── lib
 │    ├── src
 │    │    ├── constants.dart
 │    │    ├── messages.dart
 │    │    └── utils.dart
 │    └── my_twilio.dart
 └── pubspec.yaml

Setting up the package structure

In the location of your choice, create a new folder called my_twilio.

In the my_twilio folder, create a text file named pubspec.yaml and two folders named lib and example.

In the example folder, create a text file named send_sms.dart.

Inside lib create a text file named my_twilio.dart and a folder named src.

Inside the src folder create three text files named messages.dart, constants.dart, and utils.dart.

Creating the source code

Insert the following code in pubspec.yaml file:

# pubspec.yaml 

name: my_twilio

version: 0.1.0

environment:
  sdk: '>=2.6.0 <3.0.0'

dependencies:
  http: ^0.12.0+4

The pubspec.yaml file contains some metadata about the package:

  • every package needs a name
  • every package has a version
  • a package might only work with certain versions of the Dart SDK and you can specify those versions using an SDK constraint
  • the dependencies section lists each package that your package needs in order to work

Insert the following Dart code in the my_twilio.dart file in the lib directory:

// my_twilio.dart

import './src/messages.dart' show Messages;

class MyTwilio {
  final String _accountSid;
  final String _authToken;

  const MyTwilio(this._accountSid, this._authToken);

  Messages get messages => Messages(_accountSid, _authToken);
}

The code in the my_twilio.dart file is the library exposed by your my_twilio package, it can be imported in other packages or Dart programs. It starts with an import of the Messages class defined in ./src/messages.dart. It defines a MyTwilio class with two fields, _accountSid and _authToken, a MyTwilio() constructor and a getter, messages which returns a Messages class. As you’ll see below, the Messages class defines a method named create() which does the job of calling Twilio’s API to send the text message.

Insert the following code in the constants.dart file in the lib directory:

// constants.dart

const String TWILIO_SMS_API_BASE_URL = 'https://api.twilio.com/2010-04-01';

To keep the code clean and organized, constants are kept in constants.dart. For now, a constant of type String is defined to hold the value of Twilio’s SMS API base URL. If needed, you can add more constants here.

Insert the following code in the utils.dart file in the lib directory:

// utils.dart

import 'dart:convert' show base64, utf8;

// returns base64 encoded Twilio credentials
// used in authorization headers of http requests
String toAuthCredentials(String accountSid, String authToken) =>
    base64.encode(utf8.encode(accountSid + ':' + authToken));

The code in utils.dart defines a function named toAuthCredentials() which returns your Twilio credentials as a base64 encoded string ready to be used in the authorization header of an HTTP request to Twilio’s API. Encoding is done with the help of the dart:convert library, which is part of Dart’s core libraries.

Insert the following Dart code in the messages.dart file in the lib folder:

// messages.dart

import 'dart:convert' show json;

import 'package:http/http.dart' as http;

import './constants.dart';
import './utils.dart';

class Messages {
  final String _accountSid;
  final String _authToken;

  const Messages(this._accountSid, this._authToken);

  Future<Map> create(data) async {
    var client = http.Client();

    var url =
        '${TWILIO_SMS_API_BASE_URL}/Accounts/${_accountSid}/Messages.json';

    try {
      var response = await client.post(url, headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': 'Basic ' + toAuthCredentials(_accountSid, _authToken)
      }, body: {
        'From': data['from'],
        'To': data['to'],
        'Body': data['body']
      });

      return (json.decode(response.body));
    } catch (e) {
      return ({'Runtime Error': e});
    } finally {
      client.close();
    }
  }
}

The messages.dart code imports the code from constants.dart, utils.dart, and the external library package http from pub.dev (the primary public repository for Dart packages). http contains a set of high-level functions and classes that make it easy to consume HTTP resources.

The code in messages.dart defines a Messages class which contains two fields, _accountSid and _authToken, a constructor Messages(), and a create() method. When an instance of Messages is created, values for your Twilio credentials are stored in _accountSid and _authToken. When create() is invoked an authenticated HTTP POST request is made to Twilio’s API to send the text message.

With the help of the http package, an http.Client() instance is created and client.post() is invoked. This is an asynchronous HTTP request and the async/await keywords are used to instruct the program that it has to wait for the response from Twilio.

Installing dependencies

The pub tool is the package manager for the Dart programming language. The pub get command gets all the dependencies listed in the pubspec.yaml file in the current working directory, as well as their transitive dependencies. A transitive dependency is a dependency that your package indirectly uses because one of its dependencies requires it.

my_twilio depends on the http package hosted at pub.dev. You’ve let pub know about this in the dependencies section in pubspec.yaml:

...
dependencies:
  http: ^0.12.0+4
...

Ellipsis (“...”) indicates a section of code redacted for brevity or emphasis.

Open a console window in the my_twilio directory and run the following command-line instruction:

pub get

The pub utility will download and save the http package and its dependencies.

Testing the package with an example

Copy the following code to the send_sms.dart file in the example folder:

// send_sms.dart

import 'dart:io' show Platform;

import 'package:my_twilio/my_twilio.dart';

Future<void> main() async {
  // See http://twil.io/secure for important security information
  var _accountSid = Platform.environment['TWILIO_ACCOUNT_SID'];
  var _authToken = Platform.environment['TWILIO_AUTH_TOKEN'];

  // Your Account SID and Auth Token from www.twilio.com/console
  // You can skip this block if you store your credentials in environment variables
  _accountSid ??= 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
  _authToken ??= 'your_auth_token';

  // Create an authenticated client instance for Twilio API
  var client = new MyTwilio(_accountSid, _authToken);

  // Send a text message
  // Returns a Map object (key/value pairs)
  Map message = await client.messages.create({
    'body': 'Hello from Dart!',
    'from': '+12345678901', // a valid Twilio number
    'to': '+12345678902' // your phone number
  });

  // access individual properties using the square bracket notation
  // for example print(message['sid']);
  print(message);
}

The send_sms.dart code imports the my_twilio library, creates an instance of the MyTwilio class, then calls client.messages.create() to send a text message. The argument to create() is a collection of key/value pairs for 'body': the text of the message; 'from', a valid Twilio number; and 'to', the phone number where the message should be received.

Credentials for access to Twilio’s API are first taken from environment variables with an attempt like this:

var _accountSid = Platform.environment['TWILIO_ACCOUNT_SID'];

If environment variables are not defined, _accountSid will hold a value of null.

With _accountSid ??= 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; _accountSid is tested for null, and if it is null the value from the right side of the ??= operator is assigned to it. If you’ve defined environment variables for your Twilio credentials this line won’t have any effect.

Replace _accountSid and _authToken with your Twilio user secret values—or go with the better option and set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN environment variables.

Replace the from and to phone numbers with your Twilio phone number and the mobile phone number you used to register for your Twilio account.

Optionally, replace the value for body with your own message.

To run the example app from the command line, use the Dart VM by running the dart command, as follows.

Open a console window  in the my_twilio folder and execute the following command-line instruction:

dart ./example/send_sms.dart

If the program executes successfully you should see command line output similar to the following:

{sid: SMf4a886e1db45417687035967922ca19c, date_created: Mon, 09 Mar 2020 20:19:50 +0000, date_updated: Mon, 09 Mar 2020 20:19:50 +0000, date_sent: null, account_sid: AC40000000000000000000000000000006, to: +16175551212, from: +12125551212, messaging_service_sid: null, body: Hello again from Dart., status: queued, num_segments: 1, num_media: 0, direction: outbound-api, api_version: 2010-04-01, price: null, price_unit: USD, error_code: null, error_message: null, uri: /2010-04-01/Accounts/AC40000000000000000000000000000006/Messages/SMf40000000045417687035967922ca19c.json, subresource_uris: {media: /2010-04-01/Accounts/AC40000000000000000000000000000006/Messages/SMf40000000045417687035967922ca19c/Media.json}}

Shortly thereafter you should receive an SMS message on your mobile device. It will be from your Twilio phone number and the message will be the value supplied for body above.

Compiling a standalone executable with dart2native

The dart2native tool is a compiler which you can use to compile a Dart program to machine code. You can make an executable version of the my_twilio package in a few steps.

Open a console window in the my_twilio folder.

On Windows, execute the following command-line instruction:

dart2native ./example/send_sms.dart -o ./example/send_sms.exe

On macOS, execute:

dart2native ./example/send_sms.dart -o ./example/send_sms

The compiler will create send_sms.exe on Windows or send_sms on macOS) inside the example folder.

You can run this executable on any computer which has the same operating system as the one it was created on. At the moment, cross-compilation is not supported.  Other than that, the executable contains everything it needs to run. (The Dart SDK is not needed.)

Using the my_twilio package in another Dart project

In the same folder where you created the my_twilio folder, create a new folder named my_dart_project.

In the my_dart_project folder, create a text file named pubspec.yaml and two folders named bin and lib.

In the bin folder, create a text file named main.dart. This will be the entry point for your app.

Copy the following code to the pubspec.yaml file:

# pubspec.yaml

name: my_dart_project

version: 0.1.0

environment:
  sdk: '>=2.6.0 <3.0.0'
  
dependencies:
  my_twilio:
    path: ../my_twilio

You can add a local package as a dependency to your project by specifying the relative path to the package in pubspec.yaml dependencies section.This tells the pub utility how the package can be located.

Open a console window in the my_dart_project folder and run the following command-line instruction:

pub get

Once the pub utility finishes running you can import my_twilio in your Dart code.

Insert the following Dart code into the main.dart file:

// main.dart

import 'dart:io' show Platform;

import 'package:my_twilio/my_twilio.dart';

Future<void> main() async {
  // See http://twil.io/secure for important security information
  var _accountSid = Platform.environment['TWILIO_ACCOUNT_SID'];
  var _authToken = Platform.environment['TWILIO_AUTH_TOKEN'];

  // Your Account SID and Auth Token from www.twilio.com/console
  // You can skip this block if you store your credentials in environment variables
  _accountSid ??= 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
  _authToken ??= 'your_auth_token';

  // Create an authenticated client instance for Twilio API
  var client = new MyTwilio(_accountSid, _authToken);

  // Send a text message
  // Returns a Map object (key/value pairs)
  Map message = await client.messages.create({
    'body': 'Hello from Dart.',
    'from': '+12345678901', // a valid Twilio number
    'to': '+12345678902' // your phone number
  });

  // access individual properties using the square bracket notation
  // for example print(message['sid']);
  print(message);
}

Replace _accountSid and _authToken with your Twilio user secret values—or go with the better option and set TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN environment variables.

Replace the from and to phone numbers with your Twilio phone number and the mobile phone number you used to register for your Twilio account.

Optionally, replace the value for body with your own message.

Testing the my_dart_project executable

Open a console window in the my_dart_project folder and execute the following command-line instruction:

dart ./bin/main.dart

You should see console output similar to that produced by the my_twilio package and you should receive an SMS message on your mobile phone.

Creating a standalone executable that takes arguments

You’re going to modify main.dart to create an sms.exe, on Windows, or a sms, on macOS, command-line executable and pass in --from, --to, and --body arguments so it can be run like the following example:

sms.exe --from +12345678901 --to +12345678902 --body 'Hello from Dart!'

Begin by importing the hosted package args, which has a library, arg_parser.dart, you’ll use to parse the list of arguments and return the results.

Add the args package as a dependency in the pubspec.yaml file by modifying the dependencies section so it looks like the following code block. Note that the indentation shown is required.

...
dependencies:
  my_twilio:
    path: ../my_twilio
  args: ^1.5.0

In a console window in the my_dart_project directory, run pub get to get the args package.

At the top of main.dart file, import args, modify the import dart:io statement to expose the exit function and define a global variable named argResults with a type of ArgResults, as shown in the code block below.

You will use the exit() function to terminate the program if no valid arguments are passed.

You will use the argResults variable to store a collection of key/value pairs containing the program’s arguments.

import 'dart:io' show Platform, exit;

import 'package:args/args.dart';
import 'package:my_twilio/my_twilio.dart';

ArgResults argResults;
...

Modify the main() method signature to accept command-line arguments, which will be passed in as a list of strings.

At the top of the main() function, add a call to a function parseCommandLineArguments(), as shown below: 

Future<void> main(List<String> args) async {
  // ensure the required command-line arguments are present
  // then parse and save them in the argResults variable
  parseCommandLineArguments(args);
...

Define the parseCommandLineArguments() function by adding the following Dart code to the main.dart file above the main() function:

void parseCommandLineArguments(arguments) {
  final parser = new ArgParser()
    ..addOption('from',
        abbr: 'f', help: 'A valid Twilio number (in E.164 format).')
    ..addOption('to',
        abbr: 't', help: 'Your (destination) phone number (in E.164 format).')
    ..addOption('body',
        abbr: 'b',
        help: 'The text of the message. (surrounded with quotation marks)');

  try {
    argResults = parser.parse(arguments);

    // if required arguments are not present throw exception
    // usage will be printed
    if (argResults.options.isEmpty ||
        argResults['from'] == null ||
        argResults['to'] == null ||
        argResults['body'] == null) {
      throw ArgParserException;
    }
  } catch (e) {

    print('Sends a text message via Twilio\'s API\n' +
        'usage: sms --from|-f <phone number> --to|-t <phone number> --body|-b <message text>');
    print(parser.usage);
    print(
        'example: sms -f +12345678901 -t +12345678902 -b "Hey, how\'s it going?"');
    exit(1);
  }
}

The function begins by creating a parser object, an instance of the ArgParser class exposed by the args package. Using addOption() method calls, the expected command-line options are added to the parser. Then the program's arguments are parsed with a call to parser.parse(arguments) and saved to argResults. If the program is called with invalid arguments or without arguments, an ArgParserException is thrown and usage instructions are printed with the help of parser.usage, which contains the help text.

The last thing to change in the main() function code is to replace hard-coded values of the body, from, and to parameters in the client.messages.create() method with values taken from the argResults collection.

Replace the existing client.messages.create() function with the following code:

  Map message = await client.messages.create({
    'body': argResults['body'], // the text of the message
    'from': argResults['from'], // a valid Twilio number
    'to': argResults['to'] // your phone number
  });

If you need to verify your code changes are correct you can check them against the main.dart file in the dart-twilio-sms companion repository on GitHub.

Testing the my_dart_project package

In the my_dart_project directory, open a console window and run the following command-line instruction:

dart ./bin/main.dart --from +12345678901 --to +12345678902 --body "Hello again from Dart."

You should see console output similar to the output shown above for the my_twilio package. You should also receive an SMS message on the mobile device specified by the --to argument.

Creating a standalone executable from the my_dart_project package

On Windows, open a console window in the my_dart_project folder and execute the following command-line instruction:

dart2native ./bin/main.dart -o ./bin/sms.exe

On macOS, execute the following command:

dart2native ./bin/main.dart -o ./bin/sms

Testing the my_dart_project executable with arguments

On Windows test the compiled executable by opening a console window in the bin folder and running the program with the command-line arguments, replacing the values shown below with your Twilio phone number and your registered mobile phone number:

sms.exe --from +12345678901 --to +12345678902 --body "Hello again from Dart."

On macOS run:

./sms --from +12345678901 --to +12345678902 --body "Hello again from Dart."

You should see console output similar to the following and you should receive an SMS message on your mobile device.

{sid: SMf4a886e1db45417687035967922ca19c, date_created: Mon, 09 Mar 2020 20:19:50 +0000, date_updated: Mon, 09 Mar 2020 20:19:50 +0000, date_sent: null, account_sid: AC40000000000000000000000000000006, to: +16175551212, from: +12125551212, messaging_service_sid: null, body: Hello again from Dart., status: queued, num_segments: 1, num_media: 0, direction: outbound-api, api_version: 2010-04-01, price: null, price_unit: USD, error_code: null, error_message: null, uri: /2010-04-01/Accounts/AC40000000000000000000000000000006/Messages/SMf40000000045417687035967922ca19c.json, subresource_uris: {media: /2010-04-01/Accounts/AC40000000000000000000000000000006/Messages/SMf40000000045417687035967922ca19c/Media.json}}

Summary

By building a simple Dart library package and then using it to create a standalone command-line program to send text messages you learned how to communicate with Twilio’s Messaging API from Dart code. You could add more functionality to my_twilio to interact with other Twilio APIs such as Programmable Voice.

Additional resources

Dart Docs: Creating packages

Dart Docs: Write command-line apps

pub.dev http (library for making HTTP requests)

pub.dev args (library to parses command-line arguments)

Learn more by reading other Dart projects source code at GitHub firebase-dart, dart-sass.

Alex Baban is a Romanian-born Canadian web and mobile developer and is a Twilio Champion. Alex has been working on web sites in various capacities for over 15 years. In addition to software development, Alex has a background in electronics, with a Bachelor of Science in Electrical Engineering from the Technical University of Cluj-Napoca, Romania. Alex can be reached on Twitter @alexbaban.