This tutorial gives a basic overview over the programming language Dart

1. Dart

The Dart programming language is a general purpose programming language developed by Google. It is widely used at Google and the basis for Flutter development.

The syntax of Dart is very similar to Java and C++. Therefore, the object oriented language is very approachable for developers with knowledge in other programming languages.

Frequently, if you use Dart you also uses the following tools:

  • stagehand - provide templates for creating Dart apps

  • pub - package manager for Dart to access additional libraries

2. Installation

To be able to run or compile Dart apps you will need to have the Dart SDK installed.

Download the SDK from the Dart installation website and see the official installation instructions. Follow the instructions for your operating system.

After that the Dart SDK should be automatically added to your path.

To test the availability of dart, type dart --version in a terminal.

Linux

On Linux Dart is typically installed in /usr/lib/dart/bin. To add it to your path, add export $PATH:/usr/lib/dart/bin at the end of the .bashrc file in your home directory.

MacOS

Add /Applications/dart/dart-sdk/bin at the end of /etc/paths.

3. Development

Dart execution can be triggered from the command line. https://dartpad.dev/ provides a web user interface for writing and testing Dart code.

To develop and run a Dart application locally on your machine, you need the Dart SDK and a text editor or a IDE for Dart. Such IDE tools are:

3.1. Running a Dart application

Running a Dart program requires a file with a main() method. This file can then be executed via the following command line:

$ dart path-to-file.dart.

The compiler will automatically pick up the main() method and execute it.

3.2. Packaging / Building for production

To bundle an application Dart uses so called "snapshots". It bundles all files in your project into an executable binary file. The Dart SDK can then execute the file.

Creating a snapshot:

$ dart --snapshot=<your-snapshot-name> <your-main-file>

Then run the created snapshot:

$ dart <your-snapshot-name>

4. Programming in Dart

Every Dart app has a main method. It can be main() or main(List<String> args) if access to the command line arguments is needed.

void main() {
  print('Hello, World!');
}

Dart does not have have keywords like public, private and protected. You can define code, i.e., variables, functions, getters and setters outside of classes. Dart uses 2-character indentation by convention. The following is a very short summary of important language constructs in Dart.

4.1. Variables

Dart is a statically typed language but you can use type-inference to avoid explicit types. This means there is a var keyword that tells the compiler, that it should determine the variable type from the return value. Variables can also be manually typed, though it is favorable to use var in many cases.

var someVariable = "Hello World!";
int someOtherVariable = 1;
int _anotherVariable = 12;

Prefixing an identifier with an underscore defines the variable as private, e.g. it can not be changed outside its defining scope.

Variables can have the following modifies:

  • static - a static variables are available on the class itself instead of the instance

  • final - a final variable can be set only once, they can not be reassigned, they must be initialized before the constructor body starts

  • const - a const variable is a compile-time constant

The value of a const variable is determined at compile time and can not be changed at run time. The default value of uninitialized variables is null.

4.2. Classes

Dart support object oriented programming via the usage of classes. The following is an example class.

class Gerrit {
  String id;
  String userId;
  String subject;
}

Constructors are called upon instantiation of the class. It is valid in Dart that constructors have no body.

You can populate fields in the constructor with the this keyword.

class Gerrit {

  String id;
  String userId;
  String subject;

  Gerrit(this.id, this.userId, this.subject); (1)
}
1 Constructor requires that several parameters of type String are passed to the constructor. Using this in a constructor’s parameter list assigns values to instance variables.

The following code is equivalent to the initialization using the this keyword.

class Gerrit {

  String id;
  String userId;
  String subject;

  Gerrit(id, userId, subject) {
    this.id = id;
    this.userId = userId;
    this.subject = subject;
  }
}

You can define that parameters are optional parameters by putting them into {} braces.

class Gerrit {

  String id;
  String userId;
  String subject;

  Gerrit({this.id, this.userId, this.subject="default"});(1)
}
1 Note the curly braces. This marks the parameters as optional and the programmer can decide whether to populate the field or not. It is also possible to assign default values if the relevant parameter is not used.

You can also initialize instance variables before the constructor body runs via a initializer list. Separate initializers with commas.

class Gerrit {
  String id;
  String userId;
  String subject;

  Gerrit.fromJson(Map<String, String> json)
      : id = json['id'],
        userId = json['userId'],
        subject = json['subject'];
}

You can define a tostring method to define the String representation of your class.

class Gerrit {

  // as before

  @override                                 (1)
  String toString() => 'Gerrit: $subject';  (2)
}
1 Tell the Dart compiler that you want to override a member
2 Shorten one-line functions or methods using fat arrow (⇒) notation. Dart supports single or double quotes when specifying strings. Use string interpolation to put the value of an expression inside a string literal: ${expression}. If the expression is an identifier, you can skip the braces: $variableName.

4.2.1. Instantiation of a Class

A class can be instantiated with or without the optional new keyword

main() {
  var gerrit = Gerrit("1", "vogella" "Testing"); // using new is optional
  print(gerrit);
  final g2 = Gerrit("2", "vogella" "Testing"); (1)
}
1 if the variable value does not change, you can use final instead of var.

if optional parameters are used, you need to use named parameters to tell Dart which parameter is used.

ClassName(someVar: "Some Value");

If the members of a class are final and are initialized in the constructor it can also be made const. This allows the class to be constructed at compile time.

class ClassName {

    final String someVar;

    const ClassName({this.someVar});
}

4.3. Getter and setters

By default, Dart provides implicit getters and setters for all public instance variables You can provide additional getter and setter or override the implicit ones with the get and set keyword. Access to instance variable is the same, no matter if you define a settter or getter or not.

import 'dart:async';

class StringCreator {
  final StreamController _controller = StreamController();

  StringCreator() {
    Timer.periodic(Duration(seconds: 5), (t) => _push());
  }

  _push() {
    _controller.sink.add("Lars");
  }
  Stream<String> get stream => _controller.stream; (1)
}
1 defines a new accessor for the stream attribute, allows clients to access a new stream attribute

4.3.1. Inheritance and implicit interfaces

Every class implicitly defines an interface containing all the instance members of the class and of any interfaces it implements.

class MySubTypeGerrit implements Gerrit {
  // more methods
}

Dart has single inheritance.

class FixedUserGerrit extends Gerrit {
  FixedUserGerrit(String id, String description)
      : super(id, "vogella", description);
}

Dart supports Mixins for reusing code in multiple class hierarchies.

4.4. Using factory constructors

Using the factory keyword you can define a factory method which returns certain instances. If you name your factory with the default constructor name you have to have a named constructor. Using _ makes the named constructor internal.

class Gerrit {

  String id;
  String userId;
  String subject;
  factory Gerrit (int i) {
    // create instance of Gerrit based on the int information and return it
    return Gerrit._internal(i.toString(), "MyUser", "Topic 1");
  }

  Gerrit._internal({this.id, this.userId, this.subject="default"});(1)
}

4.5. Comments

There are three types of comments:

// Single line comment

/*
Multi line
comment
*/

/// Documentation comments
/// These allow to reference types or variables via [square-brackets]
When you use documentation comments (///) generating Dartdoc can easily be done by using the dartdoc tool. This is distributed with the SDK.

4.6. Parameters

A function can have two types of parameters: required and optional. Some APIs—notably Flutter widget constructors — use only named parameters, even for parameters that are mandatory. When calling a function, you can specify named parameters using paramName: value. Although named parameters are a kind of optional parameter, you can annotate them with @required to indicate that the parameter is mandatory — that users must provide a value for the parameter. To use the @required annotation, depend on the meta package and import package:meta/meta.dart.

4.6.1. Inheritance

Dart classes have mixin-based inheritance. Every object is an instance of a class, and all classes inherit from Object.

4.7. Strings

Dart supports multi-line strings.

main() {
  var user = 'Test Joe';
  var message = """
      $user!
      Welcome to Programming Dart!
      """;
  print(message);
}

4.8. Lists and Maps

Dart provides native support for list and maps and allows to construct it directly

For example, the following is a definition of list in which each list element is a map.

final dummySnapshot = [
 {"name": "Filip", "votes": 15},
 {"name": "Justin", "votes": 1},
];

4.9. Method cascades

Dart supports writing fluent API via method cascades. You can call any method after each other via the .. operator.

class Test {
  void doIt(){}
  String returnIt() {return "s";}

}
main() {
  var t = Test();
  t..returnIt()..doIt();
  t..doIt()..returnIt();
}

4.10. Null aware operations

Dart supports null aware operations:

  • expression1 ?? expression2 - evaluates to expression1 if this is not null, otherwise it evaluates to expression2

  • x ??= expression - assigns x to expression only if x is null

  • x?.p - evaluates to x.p if x is not null, otherwise to null

main() {
  var x = null;
  print (x ?? "Default text");

  x ??= "Hello";

  print(x?.toString());
}

4.11. Casting in Dart

The instanceof-operator is called is in Dart.

class Foo { }

main() {
  var foo = new Foo();
  if (foo is Foo) {
    print("It is a foo");
  }
}

You can also explicit cast via the as operator.

(emp as Person).firstName = 'Bob';

4.12. Extension methods

Via extension methods you can add functionality to existing libraries.

For this, you need to require at least Dart 2.7.0 in your pubspec.yaml.

environment:
  sdk: ">=2.7.0 <3.0.0"

The syntax for implementing an extension is the following:

extension <extension name> on <type> {
  (<member definition>)*
}

For example, you can add a method to the String data type.

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
}

  main() {
   int i = "12".parseInt();
  }

You can import extensions from a file and use show and hide to allow or permit certain extensions.

// Defines the String extension method parseInt().
import 'string_apis.dart';

// Also defines parseInt(), but hiding NumberParsing2
// hides that extension method.
import 'string_apis_2.dart' hide NumberParsing2;

If both extensions have the same name, then you might need to import using a prefix:

import 'string_apis_3.dart' as rad;

// Use the ParseNumbers extension from string_apis_3.dart.
print(rad.NumberParsing('42').parseInt());

5. Asynchronous processing in Dart with futures, async and await

Asynchronous operations allows your program to stay responsive which performing some tasks. Very potential long running operation should be done asynchronously:

  • Fetching data over a network

  • Writing to a database

  • Reading data from a file

Dart provides strong support for asynchronous programming with the following build in structures: * Futures * Streams * async / await

5.1. Isolates

Dart uses Isolates to run threads in. Each isolate has its own memory and event loop and isolates can only communicate with each other by sending messages to them. This brings the advantage that garbage collections can be done on an isolate basis.

5.2. Future

To perform asynchronous operations in Dart, you can use the Future class and the async and await keywords. A future represents the result of an asynchronous operation.

Lots of standard Dart libraries calls, return a Future as return type, for example:

  • http.get

  • SharedPreference.getInstance()

A future can have two states:

  • Uncompleted - When you call a function that returns a future, the function queues up the operation and returns an uncompleted future

  • Completed - When a future’s operation finishes, the future completes with a value or with an error

A Future<T> instance produces a value of type T. If a future doesn’t produce a usable value, then the future’s type is Future<void>. If the function doesn’t explicitly return a value, then the return type also Future<void>

The following is an example for the usage of futures. The "Another output" is written first to the console and after the delay "Testing futures".

Future<void> printSomeThingDelayed() {
  return Future.delayed(Duration(seconds: 3), () => print('Testing futures'));
}

main() {
  printSomeThingDelayed();
  print('Another output ...');
}

Future provides a few helper methods in case the value or error is already present:

  • Future.value

  • Future.exception

For mocking slow data, you can use the delayed method.

* Future.delayed(Duration(seconds:5), () => 12,);

Future allows to register a callback via the then method which is called then the Future is completed with a value. Then also returns a Future so future calls can be chained. The catchError method allows to register a hook for the error case. The whenComplete method is used to executed code after the complete or error as been received an processed.

Here is a complete example:

void main() {
  var userIdFuture = getUserId();
  // register callback which is executed once the future is completed
  userIdFuture.then((userId) => print(userId));

  // Hello wird vor 77 ausgegeben, 77 wird erst nach 3 Sekunden ausgegeben
  print('Hello');
}

// method which computes a future
Future<int> getUserId() {
  // simulate a long network call
  return Future.delayed(Duration(seconds: 3), () => 77);
}

5.3. Await and async

The async keyword is used to define a function as asynchronous The await keywords allows to wait for the result of an asynchronous function and works only in async functions.

For example, you can declare your main method as async and await a asynchronous result.

Future<void> printSomeThingDelayed() {
  return Future.delayed(Duration(seconds: 3), () => print('Testing futures'));
}

main() async {
  await printSomeThingDelayed(); // blocks until the result returns
  print('Another output ...');
}

Here is a complete example:

void main() {
  var userIdFuture = getUserId();
  // register callback which is executed once the future is completed
  userIdFuture.then((userId) => print(userId));

  // hello is written first to the console and after 3 seconds 77
  print('Hello');
}

// method which computes a future
Future<int> getUserId() {
  // simulate a long network call
  return Future.delayed(Duration(seconds: 3), () => 77);
}

Future<String> getUserName() async {
  var userId = await getUserId();
  return Future.delayed(Duration(seconds: 1), () => "Username $userId");
}

To handle errors in async functions you use try-catch.

5.4. FutureBuilder

The FutureBuilder widget allows to use a future directly and to build its UI based on the state of the widget.

FutureBuilder<String>(
  future: _calculation, // a previously-obtained Future<String> or null
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
    switch (snapshot.connectionState) {
      case ConnectionState.none:
        return Text('Press button to start.');
      case ConnectionState.active:
      case ConnectionState.waiting:
        return Text('Awaiting result...');
      case ConnectionState.done:
        if (snapshot.hasError)
          return Text('Error: ${snapshot.error}');
        return Text('Result: ${snapshot.data}');
    }
    return null; // unreachable
  },
)

5.5. Streams

Streams support reactive programming. They work similar to a Future but instead of returning a single value, they return a series of events. Streams are designed for single subscriptions. You can use the asBroadcastStream method to convert this stream into a stream which supports multiple subscriptions.

You can subscribe to a Stream via the following methods:

  • listen

  • onError

  • onDone - called once the stream is closed

Streams can be manipulated via methods:

  • map - converts each element of the stream into something else

  • where - allows to filter elements of the stream

  • distinct - filters out consecutive identical values

Streams can be created via the StreamController class.

5.6. StreamBuilder in Flutter

StreamBuilder is a Flutter widgets which allows to build your UI based on the state of the stream.

6. Handling http requests in Dart

The package http/http.dart package provides an easy way of performing http requests. To use it you have to add it to your pubspec.yaml file.

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

http.Response response =
    await http.get("https://jsonplaceholder.typicode.com/todos");
if (response.statusCode == 200) {
  String body = response.body;
  print(body);
}

7. Handling JSON data in Flutter

The built-in dart package dart:convert provides a few methods for working with JSON data. The two main methods are:

  • jsonEncode(Object object): Serializes a Dart Object into a JSON string

  • jsonDecode(String source): Deserializes a JSON string into a Dart object

jsonEncode and jsonDecode are shorthands for json.Encode and json.Decode and are useful in case a local variable is called json.

To deserialize a String of JSON, use jsonDecode:

var json = '{"key": "Some Data"}';
var data = jsonDecode(json);
print(data['key']); // "Some Data"

Similarly to serialize a Dart object into a JSON String, use jsonEncode:

var data = {'key': 'Some Data'};
var json = jsonEncode(data);
print(json); // {"key": "Some Data"}

By default only the following data types are supported:

  • numbers (int, float, double, etc.)

  • boolean

  • String

  • null

  • a list

  • Map (with String keys)

However, if the value is not of any of the above types, the encoder will try calling toJson on it. You can use this to implement custom serialization logic for you objects:

class DataClass {
    int id;
    String name;

    DataClass(this.id, this.name);

    Map<String, dynamic> toJson() => { 'id': id, 'name': name };
}

var json = jsonEncode(DataClass(4, 'Some name'));
print(json); //  {"id": 4, "name": "Some name"}

If you don’t have control over the object, you can alternatively use the optional second parameter in jsonEncode, toEncodable. It takes a function that receives the object passed in and returns some custom serialization logic.

var json = jsonEncode(DataClass(6, 'Some other Name'), (DataClass value) => { 'id': id, 'name': name });
print(json); // {"id": 4, "name": "Some name"}

Here is a small example, which downloads a JSON from the Internet and converts it into a data object.

Future<List<Todo>> _getTodos() async {
  List<Todo> todos = List();
  http.Response response =
      await http.get("https://jsonplaceholder.typicode.com/todos");
  if (response.statusCode == 200) {
    String body = response.body;
    var json = jsonDecode(body);
    for (Map<String, dynamic> entry in json) {
      var userId = entry['userId'];
      todos.add(Todo(userId: userId));
    }
  }
  return todos;
}

8. Exercise - Developing a Dart Command Line Application

This exercise demonstrates how to build a command line Dart application.

In this exercise you access a REST API from the open source Gerrit server and print the result to the command line. Gerrit is a code review server originally developed for the Android system.

In this example we access the Gerrit server of the Eclipse foundation.

8.1. Project Setup

Create a new folder (aka directory) called gerritreader.

Create the following files inside the new directory:

  • lib/main.dart

  • lib/change.dart

  • lib/fetcher.dart

  • pubspec.yaml

Paste the following contents into pubspec.yaml.

name: gerritreader
description: A Dart command line application

environment:
  sdk: '>=2.4.0 <3.0.0'

dependencies:
 http: ^0.12.0

This makes the Dark SDK and the http library available.

The Dart tooling (in VSCode, Eclipse or IntelliJ) automatically synchronizes the dependencies for you. If this does not work, use the $ pub get command in the main directory to update your dependencies.

8.2. Data Model

Create the following class in the lib/change.dart file which represents a Gerrit change request.

class Change {
  String id;
  String userId;
  String subject;
  String project;
  String status;
  DateTime updated;

  Change({this.userId, this.id, this.status, this.subject, this.project, this.updated});

  @override
  String toString() {
    return "Subject " + subject + " Updated " + updated.toString();
  }

  factory Change.fromJson(Map<String, dynamic> json) { (1)
   return Change(
      // userId: account['_account_id'],
      userId : json['owner']['_account_id'].toString(),
      id: json['change_id'],
      // more complex validation / conversion could be moved to a static method
      updated: DateTime.parse((json['updated']).substring(0, 10)),
      subject: json['subject'],
      status: json['status'],
      project: json['project'],
    );
  }
}
1 The factory method allows to create a Change object from a JSON file.

8.3. Fetching the Data

We use the following URL for testing platform/eclipse.platform.ui project:

The result contains all open Gerrits for the Eclipse Platform UI project.

Performing http requests in Dart is pretty straightforward. The Dart core libraries contain a HttpClient that allows access to many operations of the HTTP protocol.

In this example we use the http library which makes http calls even simpler.

Create the following class in the lib/fetcher.dart file.

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

const CHANGES_URL =
    'https://git.eclipse.org/r/changes/?q=status:open+project:platform/eclipse.platform.ui';

// set to false in case you don't want the JSON printed to the command line
const DEBUG = true;

class Fetcher { (1)
  Future<List<Change>> fetchChanges() async {
    http.Response reponse = await http.get(CHANGES_URL);
    var body = reponse.body.replaceAll(r")]}'", ""); (2)

    var changes = <Change>[];
    for (var json in jsonDecode(body)) {
      changes.add(Change.fromJson(json));
      if (DEBUG) {
        JsonEncoder encoder = new JsonEncoder.withIndent('  ');
        String prettyprint = encoder.convert(json);
        print(prettyprint);
      }
    }
    return changes;
  }
}
1 The result of the fetchChanges() method is a List<Change> wrapped in an asynchronous call.
2 Since the data from the Gerrit API has a special string that is not valid JSON in the first line of every response, it needs to be truncated in the client. See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output (search for )]}') for more information on this.

8.4. Using the API in the Main Method.

Now you will use the API you created.

import 'package:gerritreader/fetcher.dart';

main() async {
  Fetcher fetcher = Fetcher();
  await fetcher.fetchChanges();
}

If you see the message "Warning: Interpreting this as package URI" while running main.dart, try moving it to the bin folder, see https://github.com/dart-lang/sdk/issues/35279.

8.5. Test the Program

Run your program via your IDE, e.g. in Visual Studio code via Debug  Start Debugging.

Also run it via the command line in the lib folder via dart main.dart.

You should see the JSON printed to the Debug Console in VS Code or to the command line.

8.6. Optional: Extend the Query to Read the User Data

Extend the program so that you also read the user name via the REST API. You can use the following URI for that, replacing $userId with the userId of the Change object.

8.7. Optional: Output HTML

Put the following into a multi-line string in your main.dart file.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gerrit viewer</title>
    <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="root">
  <table style="width:100%">
  PLACEHOLDER (1)
  </table>
  </div>
</body>
</html>
1 This will be replaced by the generated HTML.

Retrieve the list of Changes. Iterate over the list and and generate a HTML list from the list of changes.

For this use the String.replace method replacing PLACEHOLDER with the generated string. For each Change element create an HTML list entry similar to the following:

<tr>
<td> Summary of the Change ID
</td>
<td> Last time updated
</td>
</tr>
</tl>

8.8. Optional: Write the HTML output to a file instead of the command line

Review https://api.dartlang.org/stable/2.6.1/dart-io/File-class.html to learn how to write Files in Dart and save the generated output as opengerrits.html.

9. Dart resources