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?
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.
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.
Define a Bicycle class
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() {
}
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()
norBicycle
is declared aspublic
, because all identifiers are public by default. Dart doesn't have keywords forpublic
,private
, orprotected
. - 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
Add the following constructor to the
Bicycle
class:
Bicycle(this.cadence, this.speed, this.gear);
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.
Click Format.
Instantiate and print a Bicycle instance
Add the following code to the
main()
function:
void main() {
var bike = new Bicycle(2, 0, 1);
print(bike);
}
Remove the optional
new
keyword:
var bike = Bicycle(2, 0, 1);
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 ofvar
. - The
print()
function accepts any object (not just strings). It converts it to aString
using the object'stoString()
method.
Run the example
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'
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.
Add the following
toString()
method anywhere in the Bicycle
class:
@override
String toString() => 'Bicycle: $speed mph';
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
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.
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
In the
Bicycle
constructor, remove the speed
parameter:
Bicycle(this.cadence, this.gear);
In
main()
, remove the second (speed
) parameter from the call to the Bicycle
constructor:
var bike = Bicycle(2, 1);
Change the remaining occurrences of
speed
to _speed
. (Two places)
Initialize
_speed
to 0:
int _speed = 0;
Add the following getter to the
Bicycle
class:
int get speed => _speed;
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
andgear
. 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
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.
Open the Rectangle example in DartPad.
Add a Rectangle constructor
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.
Observations
this.origin
,this.width
, andthis.height
use the shorthand trick for assigning instance variables inside a constructor's declaration.this.origin
,this.width
, andthis.height
are optional named parameters. Named parameters are enclosed in curly braces ({}
).- The
this.origin = const Point(0, 0)
syntax specifies a default value ofPoint(0,0)
for theorigin
instance variable. The specified default must be a compile-time constant. This constructor supplies default values for all three instance variables.
Improve the output
Add the following
toString()
function to the Rectangle
class:
@override
String toString() =>
'Origin: (${origin.x}, ${origin.y}), width: $width, height: $height';
Use the constructor
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());
}
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);
}
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
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 includedart:core
,dart:async
,dart:convert
, anddart:collection
.- By convention, Dart library constants are
lowerCamelCase
(for example,pi
instead ofPI)
. 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
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.';
}
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.
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 atry-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.
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;
}
Replace the first two lines of
main()
with the following code for instantiating the shapes:
final circle = Shape('circle');
final square = Shape('square');
Delete the
shapeFactory()
function that you previously added.
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.
Open the Shapes example in DartPad (or continue using your copy).
Add a
CircleMock
class that implements the Circle
interface:
class CircleMock implements Circle {}
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;
}
Observation
- Even though the
CircleMock
class doesn't define any behaviors, it's valid Dart—the analyzer raises no errors. - The
area
instance variable ofCircleMock
implements thearea
getter ofCircle
.
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));
}
}
Open the Scream example in DartPad.
The output should look like the following:
Aah!
Aaah!
Aaaah!
Aaaaaah!
Aaaaaaaaaaah!
Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah!
Observation
- When using string interpolation, the string
${'a' * length}
evaluates to "the character'a'
repeatedlength
times."
Convert imperative code to functional
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.
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!
Observations
skip(1)
skips the first value, 1, in thevalues
list literal.take(3)
gets the next 3 values—2, 3, and 5—in thevalues
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:
- async/await, which allows you to write asynchronous code as if it were synchronous. Check out this DartPad example, which animates the calculation of the first five decimals of π.
- Method cascades, where everything is a builder!
- Null aware operators
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
- Why Flutter uses Dart
- Announcing Dart 2: Optimized for client-side development
- Why I moved from Java to Dart
Resources
Websites