Intro to Dart for Java Developers

1. Introduction

Dart is the programming language for Flutter, Google's UI toolkit for building beautiful, natively compiled mobile, web, and desktop apps from a single codebase.

This codelab introduces you to Dart with a focus on features that Java developers might not expect. You can write Dart functions in 1 minute, scripts in 5 minutes, and apps in 10 minutes!

What you'll learn

  • How to create constructors
  • Different ways to specify parameters
  • When and how to create getters and setters
  • How Dart handles privacy
  • How to create factories
  • How functional programming works in Dart
  • Other core Dart concepts

What you'll need

To complete this codelab, all you need is a browser!

You'll write and run all the examples in DartPad, an interactive, browser-based tool that lets you play with Dart language features and core libraries. If you prefer, you can use an IDE instead, such as WebStorm, IntelliJ with the Dart plugin, or Visual Studio Code with the Dart Code extension.

What would you like to learn from this codelab?

I'm new to the topic, and I want a good overview. I know something about this topic, but I want a refresher. I'm looking for example code to use in my project. I'm looking for an explanation of something specific.

2. Create a simple Dart class

You'll start by building a simple Dart class with the same functionality as the Bicycle class from the Java Tutorial. The Bicycle class contains some private instance variables with getters and setters. A main() method instantiates a Bicycle and prints it to the console.

99c813a1913dcc42.png c97a12197358d545.png

Launch DartPad

This codelab provides a new DartPad instance for every set of exercises. The link below opens a fresh instance, which contains a default "Hello" example. You can continue to use the same DartPad throughout the codelab, but if you click Reset, DartPad takes you back to the default example, losing your work.

b2f84ff91b0e1396.png Open DartPad

Define a Bicycle class

b2f84ff91b0e1396.png Above the main() function, add a Bicycle class with three instance variables. Also remove the contents from main(), as shown in the following code snippet:

class Bicycle {
  int cadence;
  int speed;
  int gear;
}

void main() {
}

cf1e10b838bf60ee.png Observations

  • With this example, the Dart analyzer produces an error informing you that the variables must be initialized because they're non-nullable. You'll fix that in the next section.
  • Dart's main method is named main(). If you need access to command-line arguments, you can add them: main(List<String> args).
  • The main() method lives at the top level. In Dart, you can define code outside of classes. Variables, functions, getters, and setters can all live outside of classes.
  • The original Java example declares private instance variables using the private tag, which Dart doesn't use. You'll learn more about privacy a little later, in "Add a read-only variable."
  • Neither main() nor Bicycle is declared as public, because all identifiers are public by default. Dart doesn't have keywords for public, private, or protected.
  • Dart uses two-character indentation by convention instead of four. You don't need to worry about Dart's whitespace conventions, thanks to a handy tool called dart format. As the Dart code conventions ( Effective Dart) say, "The official whitespace-handling rules for Dart are whatever dart format produces."

Define a Bicycle constructor

b2f84ff91b0e1396.png Add the following constructor to the Bicycle class:

Bicycle(this.cadence, this.speed, this.gear);

cf1e10b838bf60ee.png Observations

  • This constructor has no body, which is valid in Dart.
  • If you forget the semicolon (;) at the end of a no-body constructor, DartPad displays the following error: "A function body must be provided."
  • Using this in a constructor's parameter list is a handy shortcut for assigning values to instance variables.
  • The code above is equivalent to the following, which uses an initializer list:
Bicycle(int cadence, int speed, int gear)
      : this.cadence = cadence,
        this.speed = speed,
        this.gear = gear;

Format the code

Reformat the Dart code at any time by clicking Format at the top of the DartPad UI. Reformatting is particularly useful when you paste code into DartPad and the justification is off.

b2f84ff91b0e1396.png Click Format.

Instantiate and print a Bicycle instance

b2f84ff91b0e1396.png Add the following code to the main() function:

void main() {
  var bike = new Bicycle(2, 0, 1);
  print(bike);
}

b2f84ff91b0e1396.png Remove the optional new keyword:

var bike = Bicycle(2, 0, 1);

cf1e10b838bf60ee.png Observation

  • The new keyword became optional in Dart 2.
  • If you know that a variable's value won't change, then you can use final instead of var.
  • The print() function accepts any object (not just strings). It converts it to a String using the object's toString() method.

Run the example

b2f84ff91b0e1396.png Execute the example by clicking Run at the top of the DartPad window. If Run isn't enabled, see the Problems section later in this page.

You should see the following output:

Instance of 'Bicycle'

cf1e10b838bf60ee.png Observation

  • No errors or warnings should appear, indicating that type inference is working, and that the analyzer infers that the statement that begins with var bike = defines a Bicycle instance.

Improve the output

While the output "Instance of ‘Bicycle'" is correct, it's not very informative. All Dart classes have a toString() method that you can override to provide more useful output.

b2f84ff91b0e1396.png Add the following toString() method anywhere in the Bicycle class:

@override
String toString() => 'Bicycle: $speed mph';

cf1e10b838bf60ee.png Observations

  • The @override annotation tells the analyzer that you are intentionally overriding a member. The analyzer raises an error if you fail to properly perform the override.
  • 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.
  • Shorten one-line functions or methods using fat arrow (=>) notation.

Run the example

b2f84ff91b0e1396.png Click Run.

You should see the following output:

Bicycle: 0 mph

Problems?Check your code.

Add a read-only variable

The original Java example defines speed as a read-only variable—it declares it as private and provides only a getter. Next, you'll provide the same functionality in Dart.

b2f84ff91b0e1396.png Open bicycle.dart in DartPad (or continue using your copy).

To mark a Dart identifier as private to its library, start its name with an underscore (_). You can convert speed to read-only by changing its name and adding a getter.

Make speed a private, read-only instance variable

b2f84ff91b0e1396.png In the Bicycle constructor, remove the speed parameter:

Bicycle(this.cadence, this.gear);

b2f84ff91b0e1396.png In main(), remove the second (speed) parameter from the call to the Bicycle constructor:

var bike = Bicycle(2, 1);

b2f84ff91b0e1396.png Change the remaining occurrences of speed to _speed. (Two places)

b2f84ff91b0e1396.png Initialize _speed to 0:

int _speed = 0;

b2f84ff91b0e1396.png Add the following getter to the Bicycle class:

int get speed => _speed;

cf1e10b838bf60ee.png Observations

  • Each variable (even if it's a number) must either be initialized or be declared nullable by adding ? to its type declaration.
  • The Dart compiler enforces library privacy for any identifier prefixed with an underscore. Library privacy generally means that the identifier is visible only inside the file (not just the class) that the identifier is defined in.
  • By default, Dart provides implicit getters and setters for all public instance variables. You don't need to define your own getters or setters unless you want to enforce read-only or write-only variables, compute or verify a value, or update a value elsewhere.
  • The original Java sample provided getters and setters for cadence and gear. The Dart sample doesn't need explicit getters and setters for those, so it just uses instance variables.
  • You might start with a simple field, such as bike.cadence, and later refactor it to use getters and setters. The API stays the same. In other words, going from a field to a getter and setter is not a breaking change in Dart.

Finish implementing speed as a read-only instance variable

b2f84ff91b0e1396.png Add the following methods to the Bicycle class:

void applyBrake(int decrement) {
  _speed -= decrement;
}

void speedUp(int increment) {
  _speed += increment;
}

The final Dart example looks similar to the original Java, but is more compact at 23 lines instead of 40:

class Bicycle {
  int cadence;
  int _speed = 0;
  int get speed => _speed;
  int gear;

  Bicycle(this.cadence, this.gear);

  void applyBrake(int decrement) {
    _speed -= decrement;
  }

  void speedUp(int increment) {
    _speed += increment;
  }

  @override
  String toString() => 'Bicycle: $_speed mph';
}

void main() {
  var bike = Bicycle(2, 1);
  print(bike);
}

Problems?Check your code.

3. Use optional parameters (instead of overloading)

The next exercise defines a Rectangle class, another example from the Java Tutorial.

The Java code shows overloading constructors, a common practice in Java where constructors have the same name, but differ in the number or type of parameters. Dart doesn't support overloading constructors and handles this situation differently, as you'll see in this section.

b2f84ff91b0e1396.png Open the Rectangle example in DartPad.

Add a Rectangle constructor

b2f84ff91b0e1396.png Add a single, empty constructor that replaces all four constructors in the Java example:

Rectangle({this.origin = const Point(0, 0), this.width = 0, this.height = 0});

This constructor uses optional named parameters.

cf1e10b838bf60ee.png Observations

  • this.origin, this.width, and this.height use the shorthand trick for assigning instance variables inside a constructor's declaration.
  • this.origin, this.width, and this.height are optional named parameters. Named parameters are enclosed in curly braces ({}).
  • The this.origin = const Point(0, 0) syntax specifies a default value of Point(0,0) for the origin instance variable. The specified default must be a compile-time constant. This constructor supplies default values for all three instance variables.

Improve the output

b2f84ff91b0e1396.png Add the following toString() function to the Rectangle class:

@override
String toString() =>
      'Origin: (${origin.x}, ${origin.y}), width: $width, height: $height';

Use the constructor

b2f84ff91b0e1396.png Replace main() with the following code to verify that you can instantiate Rectangle using only the parameters you need:

main() {
  print(Rectangle(origin: const Point(10, 20), width: 100, height: 200));
  print(Rectangle(origin: const Point(10, 10)));
  print(Rectangle(width: 200));
  print(Rectangle());
}

cf1e10b838bf60ee.png Observation

  • The Dart constructor for Rectangle is one line of code, compared to 16 lines of code for equivalent constructors in the Java version.

Run the example

You should see the following output:

Origin: (10, 20), width: 100, height: 200
Origin: (10, 10), width: 0, height: 0
Origin: (0, 0), width: 200, height: 0
Origin: (0, 0), width: 0, height: 0

Problems?Check your code.

4. Create a factory

Factories, a commonly used design pattern in Java, have several advantages over direct object instantiation, such as hiding the details of instantiation, providing the ability to return a subtype of the factory's return type, and optionally returning an existing object rather than a new object.

This step demonstrates two ways to implement a shape-creation factory:

  • Option 1: Create a top-level function.
  • Option 2: Create a factory constructor.

For this exercise, you'll use the Shapes example, which instantiates shapes and prints their computed area:

import 'dart:math';

abstract class Shape {
  num get area;
}

class Circle implements Shape {
  final num radius;
  Circle(this.radius);
  num get area => pi * pow(radius, 2);
}

class Square implements Shape {
  final num side;
  Square(this.side);
  num get area => pow(side, 2);
}

main() {
  final circle = Circle(2);
  final square = Square(2);
  print(circle.area);
  print(square.area);
}

b2f84ff91b0e1396.png Open the Shapes example in DartPad.

In the console area, you should see the computed areas of a circle and a square:

12.566370614359172
4

cf1e10b838bf60ee.png Observations

  • Dart supports abstract classes.
  • You can define multiple classes in one file.
  • dart:math is one of Dart's core libraries. Other core libraries include dart:core, dart:async, dart:convert, and dart:collection.
  • By convention, Dart library constants are lowerCamelCase (for example, pi instead of PI). If you're curious about the reasoning, see the style guideline PREFER using lowerCamelCase for constant names.
  • The following code shows two getters that compute a value: num get area => pi * pow(radius, 2); // Circle num get area => pow(side, 2); // Square

Option 1: Create a top-level function

b2f84ff91b0e1396.png Implement a factory as a top-level function by adding the following function at the highest level (outside of any class):

Shape shapeFactory(String type) {
  if (type == 'circle') return Circle(2);
  if (type == 'square') return Square(2);
  throw 'Can\'t create $type.';
}

b2f84ff91b0e1396.png Invoke the factory function by replacing the first two lines in the main() method:

  final circle = shapeFactory('circle');
  final square = shapeFactory('square');

Run the example

The output should look the same as before.

cf1e10b838bf60ee.png Observations

  • If the function is called with any string other than 'circle' or 'square', it throws an exception.
  • The Dart SDK defines classes for many common exceptions, or you can implement the Exception class to create more specific exceptions or (as in this example) you can throw a string that describes the problem encountered.
  • When an exception is encountered, DartPad reports Uncaught. To see information that's more helpful, wrap the code in a try-catch statement and print the exception. As an optional exercise, check out this DartPad example.
  • To use a single quote inside a string, either escape the embedded quote using slash ('Can\'t create $type.') or specify the string with double quotes ("Can't create $type.").

Problems?Check your code.

Option 2: Create a factory constructor

Use Dart's factory keyword to create a factory constructor.

b2f84ff91b0e1396.png Add a factory constructor to the abstract Shape class:

abstract class Shape {
  factory Shape(String type) {
    if (type == 'circle') return Circle(2);
    if (type == 'square') return Square(2);
    throw 'Can\'t create $type.';
  }
  num get area;
}

b2f84ff91b0e1396.png Replace the first two lines of main() with the following code for instantiating the shapes:

  final circle = Shape('circle');
  final square = Shape('square');

b2f84ff91b0e1396.png Delete the shapeFactory() function that you previously added.

cf1e10b838bf60ee.png Observation

  • The code in the factory constructor is identical to the code used in the shapeFactory() function.

Problems?Check your code.

5. Implement an interface

The Dart language doesn't include an interface keyword because every class defines an interface.

b2f84ff91b0e1396.png Open the Shapes example in DartPad (or continue using your copy).

b2f84ff91b0e1396.png Add a CircleMock class that implements the Circle interface:

class CircleMock implements Circle {}

b2f84ff91b0e1396.png You should see a "Missing concrete implementations" error because CircleMock doesn't inherit the implementation of Circle—it only uses its interface. Fix this error by defining the area and radius instance variables:

class CircleMock implements Circle {
  num area = 0;
  num radius = 0;
}

cf1e10b838bf60ee.png Observation

  • Even though the CircleMock class doesn't define any behaviors, it's valid Dart—the analyzer raises no errors.
  • The area instance variable of CircleMock implements the area getter of Circle.

Problems?Check your code.

6. Use Dart for functional programming

In functional programming you can do things like the following:

  • Pass functions as arguments.
  • Assign a function to a variable.
  • Deconstruct a function that takes multiple arguments into a sequence of functions that each take a single argument (also called currying).
  • Create a nameless function that can be used as a constant value (also called a lambda expression; lambda expressions were added to Java in the JDK 8 release).

Dart supports all those features. In Dart, even functions are objects and have a type, Function. This means that functions can be assigned to variables or passed as arguments to other functions. You can also call an instance of a Dart class as if it were a function, as in this example.

The following example uses imperative (not functional-style) code:

String scream(int length) => "A${'a' * length}h!";

main() {
  final values = [1, 2, 3, 5, 10, 50];
  for (var length in values) {
    print(scream(length));
  }
}

b2f84ff91b0e1396.png Open the Scream example in DartPad.

The output should look like the following:

Aah!
Aaah!
Aaaah!
Aaaaaah!
Aaaaaaaaaaah!
Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah!

cf1e10b838bf60ee.png Observation

  • When using string interpolation, the string ${'a' * length} evaluates to "the character 'a' repeated length times."

Convert imperative code to functional

b2f84ff91b0e1396.png Remove the imperative for() {...} loop in main() and replace it with a single line of code that uses method chaining:

  values.map(scream).forEach(print);

Run the example

The functional approach prints the same six screams as the imperative example.

Problems?Check your code.

Use more Iterable features

The core List and Iterable classes support fold(), where(), join(), skip(), and more. Dart also has built-in support for maps and sets.

b2f84ff91b0e1396.png Replace the values.map() line in main() with the following:

  values.skip(1).take(3).map(scream).forEach(print);

Run the example

The output should look like the following:

Aaah!
Aaaah!
Aaaaaah!

cf1e10b838bf60ee.png Observations

  • skip(1)skips the first value, 1, in the values list literal.
  • take(3)gets the next 3 values—2, 3, and 5—in the values list literal.
  • The remaining values are skipped.

Problems?Check your code.

7. Congratulations!

By completing this codelab, you gained knowledge of some differences between Java and Dart. Dart is easy to learn and, in addition, its core libraries and rich set of available packages increase your productivity. Dart scales well to large apps. Hundreds of Google engineers use Dart to write mission-critical apps that bring in much of Google's revenue.

Next steps

A 20-minute codelab isn't long enough to show you all of the differences between Java and Dart. For example, this codelab hasn't covered:

If you'd like to see Dart technologies in action, try the Flutter codelabs.

Learn more

You can learn much more about Dart with the following articles, resources, and websites.

Articles

Resources

Websites