Dart cheatsheet
- String interpolation
- Nullable variables
- Null-aware operators
- Conditional property access
- Collection literals
- Arrow syntax
- Cascades
- Getters and setters
- Optional positional parameters
- Named parameters
- Exceptions
- Using this in a constructor
- Initializer lists
- Named constructors
- Factory constructors
- Redirecting constructors
- Const constructors
- What's next?
The Dart language is designed to be easy to learn for coders coming from other languages, but it has a few unique features. This tutorial walks you through the most important of these language features.
The embedded editors in this tutorial have partially completed code snippets. You can use these editors to test your knowledge by completing the code and clicking the Run button. The editors also contain thorough test code; don't edit the test code, but feel free to study it to learn about testing.
If you need help, expand the Solution for... dropdown beneath each DartPad for an explanation and the answer.
String interpolation
#To put the value of an expression inside a string, use ${expression}
. If the expression is an identifier, you can omit the {}
.
Here are some examples of using string interpolation:
String | Result |
---|---|
'${3 + 2}' | '5' |
'${"word".toUpperCase()}' | 'WORD' |
'$myObject' | The value of myObject.toString() |
Exercise
#The following function takes two integers as parameters. Make it return a string containing both integers separated by a space. For example, stringify(2, 3)
should return '2 3'
.
Solution for string interpolation example
Both x
and y
are simple values, and Dart's string interpolation will handle converting them to string representations. All you need to do is use the $
operator to reference them inside single quotes, with a space in between:
String stringify(int x, int y) {
return '$x $y';
}
Nullable variables
#Dart enforces sound null safety. This means values can't be null unless you say they can be. In other words, types default to non-nullable.
For example, consider the following code. With null safety, this code returns an error. A variable of type int
can't have the value null
:
int a = null; // INVALID.
When creating a variable, add ?
to the type to indicate that the variable can be null
:
int? a = null; // Valid.
You can simplify that code a bit because, in all versions of Dart, null
is the default value for uninitialized variables:
int? a; // The initial value of a is null.
To learn more about null safety in Dart, read the sound null safety guide.
Exercise
#Declare two variables in this DartPad:
- A nullable
String
namedname
with the value'Jane'
. - A nullable
String
namedaddress
with the valuenull
.
Ignore all initial errors in the DartPad.
Solution for nullable variables example
Declare the two variables as String
followed by ?
. Then, assign 'Jane'
to name
and leave address
uninitialized:
String? name = 'Jane';
String? address;
Null-aware operators
#Dart offers some handy operators for dealing with values that might be null. One is the ??=
assignment operator, which assigns a value to a variable only if that variable is currently null:
int? a; // = null
a ??= 3;
print(a); // <-- Prints 3.
a ??= 5;
print(a); // <-- Still prints 3.
Another null-aware operator is ??
, which returns the expression on its left unless that expression's value is null, in which case it evaluates and returns the expression on its right:
print(1 ?? 3); // <-- Prints 1.
print(null ?? 12); // <-- Prints 12.
Exercise
#Try substituting in the ??=
and ??
operators to implement the described behavior in the following snippet.
Ignore all initial errors in the DartPad.
Solution for null-aware operators example
All you need to do in this exercise is replace the TODO
comments with either ??
or ??=
. Read the text above to make sure you understand both, and then give it a try:
// Substitute an operator that makes 'a string' be assigned to baz.
String? baz = foo ?? bar;
void updateSomeVars() {
// Substitute an operator that makes 'a string' be assigned to bar.
bar ??= 'a string';
}
Conditional property access
#To guard access to a property or method of an object that might be null, put a question mark (?
) before the dot (.
):
myObject?.someProperty
The preceding code is equivalent to the following:
(myObject != null) ? myObject.someProperty : null
You can chain multiple uses of ?.
together in a single expression:
myObject?.someProperty?.someMethod()
The preceding code returns null (and never calls someMethod()
) if either myObject
or myObject.someProperty
is null.
Exercise
#The following function takes a nullable string as a parameter. Try using conditional property access to make it return the uppercase version of str
, or null
if str
is null
.
Solution for conditional property access example
If this exercise wanted you to conditionally lowercase a string, you could do it like this: str?.toLowerCase()
. Use the equivalent method to uppercase a string!
String? upperCaseIt(String? str) {
return str?.toUpperCase();
}
Collection literals
#Dart has built-in support for lists, maps, and sets. You can create them using literals:
final aListOfStrings = ['one', 'two', 'three'];
final aSetOfStrings = {'one', 'two', 'three'};
final aMapOfStringsToInts = {'one': 1, 'two': 2, 'three': 3};
Dart's type inference can assign types to these variables for you. In this case, the inferred types are List<String>
, Set<String>
, and Map<String, int>
.
Or you can specify the type yourself:
final aListOfInts = <int>[];
final aSetOfInts = <int>{};
final aMapOfIntToDouble = <int, double>{};
Specifying types is handy when you initialize a list with contents of a subtype, but still want the list to be List<BaseType>
:
final aListOfBaseType = <BaseType>[SubType(), SubType()];
Exercise
#Try setting the following variables to the indicated values. Replace the existing null values.
Solution for collection literals example
Add a list, set, or map literal after each equal sign (=
). Remember to specify the types for the empty declarations, since they can't be inferred.
// Assign this a list containing 'a', 'b', and 'c' in that order:
final aListOfStrings = ['a', 'b', 'c'];
// Assign this a set containing 3, 4, and 5:
final aSetOfInts = {3, 4, 5};
// Assign this a map of String to int so that aMapOfStringsToInts['myKey'] returns 12:
final aMapOfStringsToInts = {'myKey': 12};
// Assign this an empty List<double>:
final anEmptyListOfDouble = <double>[];
// Assign this an empty Set<String>:
final anEmptySetOfString = <String>{};
// Assign this an empty Map of double to int:
final anEmptyMapOfDoublesToInts = <double, int>{};
Arrow syntax
#You might have seen the =>
symbol in Dart code. This arrow syntax is a way to define a function that executes the expression to its right and returns its value.
For example, consider this call to the List
class's any()
method:
bool hasEmpty = aListOfStrings.any((s) {
return s.isEmpty;
});
Here's a simpler way to write that code:
bool hasEmpty = aListOfStrings.any((s) => s.isEmpty);
Exercise
#Try finishing the following statements, which use arrow syntax.
Solution for arrow syntax example
For the product, you can use *
to multiply the three values together. For incrementValue1
, you can use the increment operator (++
). For joinWithCommas
, use the join
method found in the List
class.
class MyClass {
int value1 = 2;
int value2 = 3;
int value3 = 5;
// Returns the product of the above values:
int get product => value1 * value2 * value3;
// Adds 1 to value1:
void incrementValue1() => value1++;
// Returns a string containing each item in the
// list, separated by commas (e.g. 'a,b,c'):
String joinWithCommas(List<String> strings) => strings.join(',');
}
Cascades
#To perform a sequence of operations on the same object, use cascades (..
). We've all seen an expression like this:
myObject.someMethod()
It invokes someMethod()
on myObject
, and the result of the expression is the return value of someMethod()
.
Here's the same expression with a cascade:
myObject..someMethod()
Although it still invokes someMethod()
on myObject
, the result of the expression isn't the return value—it's a reference to myObject
!
Using cascades, you can chain together operations that would otherwise require separate statements. For example, consider the following code, which uses the conditional member access operator (?.
) to read properties of button
if it isn't null
:
final button = web.document.querySelector('#confirm');
button?.textContent = 'Confirm';
button?.classList.add('important');
button?.onClick.listen((e) => web.window.alert('Confirmed!'));
button?.scrollIntoView();
To instead use cascades, you can start with the null-shorting cascade (?..
), which guarantees that none of the cascade operations are attempted on a null
object. Using cascades shortens the code and makes the button
variable unnecessary:
web.document.querySelector('#confirm')
?..textContent = 'Confirm'
..classList.add('important')
..onClick.listen((e) => web.window.alert('Confirmed!'))
..scrollIntoView();
Exercise
#Use cascades to create a single statement that sets the anInt
, aString
, and aList
properties of a BigObject
to 1
, 'String!'
, and [3.0]
(respectively) and then calls allDone()
.
Solution for cascades example
The best solution for this exercise starts with obj..
and has four assignment operations chained together. Start with return obj..anInt = 1
, then add another cascade (..
) and start the next assignment.
BigObject fillBigObject(BigObject obj) {
return obj
..anInt = 1
..aString = 'String!'
..aList.add(3)
..allDone();
}
Getters and setters
#You can define getters and setters whenever you need more control over a property than a simple field allows.
For example, you can make sure a property's value is valid:
class MyClass {
int _aProperty = 0;
int get aProperty => _aProperty;
set aProperty(int value) {
if (value >= 0) {
_aProperty = value;
}
}
}
You can also use a getter to define a computed property:
class MyClass {
final List<int> _values = [];
void addValue(int value) {
_values.add(value);
}
// A computed property.
int get count {
return _values.length;
}
}
Exercise
#Imagine you have a shopping cart class that keeps a private List<double>
of prices. Add the following:
- A getter called
total
that returns the sum of the prices - A setter that replaces the list with a new one, as long as the new list doesn't contain any negative prices (in which case the setter should throw an
InvalidPriceException
).
Ignore all initial errors in the DartPad.
Solution for getters and setters example
Two functions are handy for this exercise. One is fold
, which can reduce a list to a single value (use it to calculate the total). The other is any
, which can check each item in a list with a function you give it (use it to check if there are any negative prices in the prices
setter).
/// The total price of the shopping cart.
double get total => _prices.fold(0, (e, t) => e + t);
/// Set [prices] to the [value] list of item prices.
set prices(List<double> value) {
if (value.any((p) => p < 0)) {
throw InvalidPriceException();
}
_prices = value;
}
Optional positional parameters
#Dart has two kinds of function parameters: positional and named. Positional parameters are the kind you're likely familiar with:
int sumUp(int a, int b, int c) {
return a + b + c;
}
// ···
int total = sumUp(1, 2, 3);
With Dart, you can make these positional parameters optional by wrapping them in brackets:
int sumUpToFive(int a, [int? b, int? c, int? d, int? e]) {
int sum = a;
if (b != null) sum += b;
if (c != null) sum += c;
if (d != null) sum += d;
if (e != null) sum += e;
return sum;
}
// ···
int total = sumUpToFive(1, 2);
int otherTotal = sumUpToFive(1, 2, 3, 4, 5);
Optional positional parameters are always last in a function's parameter list. Their default value is null unless you provide another default value:
int sumUpToFive(int a, [int b = 2, int c = 3, int d = 4, int e = 5]) {
// ···
}
void main() {
int newTotal = sumUpToFive(1);
print(newTotal); // <-- prints 15
}
Exercise
#Implement a function called joinWithCommas()
that accepts one to five integers, then returns a string of those numbers separated by commas. Here are some examples of function calls and returned values:
Function call | Returned value |
---|---|
joinWithCommas(1) | '1' |
joinWithCommas(1, 2, 3) | '1,2,3' |
joinWithCommas(1, 1, 1, 1, 1) | '1,1,1,1,1' |
Solution for positional parameters example
The b
, c
, d
, and e
parameters are null if they aren't provided by the caller. The important thing, then, is to check whether those arguments are null
before you add them to the final string.
String joinWithCommas(int a, [int? b, int? c, int? d, int? e]) {
var total = '$a';
if (b != null) total = '$total,$b';
if (c != null) total = '$total,$c';
if (d != null) total = '$total,$d';
if (e != null) total = '$total,$e';
return total;
}
Named parameters
#Using a curly brace syntax at the end of the parameter list, you can define parameters that have names.
Named parameters are optional unless they're explicitly marked as required
.
void printName(String firstName, String lastName, {String? middleName}) {
print('$firstName ${middleName ?? ''} $lastName');
}
void main() {
printName('Dash', 'Dartisan');
printName('John', 'Smith', middleName: 'Who');
// Named arguments can be placed anywhere in the argument list.
printName('John', middleName: 'Who', 'Smith');
}
As you might expect, the default value of a nullable named parameter is null
, but you can provide a custom default value.
If the type of a parameter is non-nullable, then you must either provide a default value (as shown in the following code) or mark the parameter as required
(as shown in the constructor section).
void printName(String firstName, String lastName, {String middleName = ''}) {
print('$firstName $middleName $lastName');
}
A function can't have both optional positional and named parameters.
Exercise
#Add a copyWith()
instance method to the MyDataObject
class. It should take three named, nullable parameters:
int? newInt
String? newString
double? newDouble
Your copyWith()
method should return a new MyDataObject
based on the current instance, with data from the preceding parameters (if any) copied into the object's properties. For example, if newInt
is non-null, then copy its value into anInt
.
Ignore all initial errors in the DartPad.
Solution for named parameters example
The copyWith
method shows up in a lot of classes and libraries. Yours should do a few things: use optional named parameters, create a new instance of MyDataObject
, and use the data from the parameters to fill it (or the data from the current instance if the parameters are null). This is a chance to get more practice with the ??
operator!
MyDataObject copyWith({int? newInt, String? newString, double? newDouble}) {
return MyDataObject(
anInt: newInt ?? this.anInt,
aString: newString ?? this.aString,
aDouble: newDouble ?? this.aDouble,
);
}
Exceptions
#Dart code can throw and catch exceptions. In contrast to Java, all of Dart's exceptions are unchecked. Methods don't declare which exceptions they might throw and you aren't required to catch any exceptions.
Dart provides Exception
and Error
types, but you're allowed to throw any non-null object:
throw Exception('Something bad happened.');
throw 'Waaaaaaah!';
Use the try
, on
, and catch
keywords when handling exceptions:
try {
breedMoreLlamas();
} on OutOfLlamasException {
// A specific exception
buyMoreLlamas();
} on Exception catch (e) {
// Anything else that is an exception
print('Unknown exception: $e');
} catch (e) {
// No specified type, handles all
print('Something really unknown: $e');
}
The try
keyword works as it does in most other languages. Use the on
keyword to filter for specific exceptions by type, and the catch
keyword to get a reference to the exception object.
If you can't completely handle the exception, use the rethrow
keyword to propagate the exception:
try {
breedMoreLlamas();
} catch (e) {
print('I was just trying to breed llamas!');
rethrow;
}
To execute code whether or not an exception is thrown, use finally
:
try {
breedMoreLlamas();
} catch (e) {
// ... handle exception ...
} finally {
// Always clean up, even if an exception is thrown.
cleanLlamaStalls();
}
Exercise
#Implement tryFunction()
below. It should execute an untrustworthy method and then do the following:
- If
untrustworthy()
throws anExceptionWithMessage
, calllogger.logException
with the exception type and message (try usingon
andcatch
). - If
untrustworthy()
throws anException
, calllogger.logException
with the exception type (try usingon
for this one). - If
untrustworthy()
throws any other object, don't catch the exception. - After everything's caught and handled, call
logger.doneLogging
(try usingfinally
).
Solution for exceptions example
This exercise looks tricky, but it's really one big try
statement. Call untrustworthy
inside the try
, and then use on
, catch
, and finally
to catch exceptions and call methods on the logger.
void tryFunction(VoidFunction untrustworthy, Logger logger) {
try {
untrustworthy();
} on ExceptionWithMessage catch (e) {
logger.logException(e.runtimeType, e.message);
} on Exception {
logger.logException(Exception);
} finally {
logger.doneLogging();
}
}
Using this
in a constructor
#Dart provides a handy shortcut for assigning values to properties in a constructor: use this.propertyName
when declaring the constructor:
class MyColor {
int red;
int green;
int blue;
MyColor(this.red, this.green, this.blue);
}
final color = MyColor(80, 80, 128);
This technique works for named parameters, too. Property names become the names of the parameters:
class MyColor {
// ...
MyColor({required this.red, required this.green, required this.blue});
}
final color = MyColor(red: 80, green: 80, blue: 80);
In the preceding code, red
, green
, and blue
are marked as required
because these int
values can't be null. If you add default values, you can omit required
:
MyColor([this.red = 0, this.green = 0, this.blue = 0]);
// or
MyColor({this.red = 0, this.green = 0, this.blue = 0});
Exercise
#Add a one-line constructor to MyClass
that uses this.
syntax to receive and assign values for all three properties of the class.
Ignore all initial errors in the DartPad.
Solution for `this` example
This exercise has a one-line solution. Declare the constructor with this.anInt
, this.aString
, and this.aDouble
as its parameters in that order.
MyClass(this.anInt, this.aString, this.aDouble);
Initializer lists
#Sometimes when you implement a constructor, you need to do some setup before the constructor body executes. For example, final fields must have values before the constructor body executes. Do this work in an initializer list, which goes between the constructor's signature and its body:
Point.fromJson(Map<String, double> json) : x = json['x']!, y = json['y']! {
print('In Point.fromJson(): ($x, $y)');
}
The initializer list is also a handy place to put asserts, which run only during development:
NonNegativePoint(this.x, this.y) : assert(x >= 0), assert(y >= 0) {
print('I just made a NonNegativePoint: ($x, $y)');
}
Exercise
#Complete the FirstTwoLetters
constructor below. Use an initializer list to assign the first two characters in word
to the letterOne
and LetterTwo
properties. For extra credit, add an assert
to catch words of less than two characters.
Ignore all initial errors in the DartPad.
Solution for initializer lists example
Two assignments need to happen: letterOne
should be assigned word[0]
, and letterTwo
should be assigned word[1]
.
FirstTwoLetters(String word)
: assert(word.length >= 2),
letterOne = word[0],
letterTwo = word[1];
Named constructors
#To allow classes to have multiple constructors, Dart supports named constructors:
class Point {
double x, y;
Point(this.x, this.y);
Point.origin() : x = 0, y = 0;
}
To use a named constructor, invoke it using its full name:
final myPoint = Point.origin();
Exercise
#Give the Color
class a constructor named Color.black
that sets all three properties to zero.
Ignore all initial errors in the DartPad.
Solution for named constructors example
The declaration for your constructor should begin with Color.black():
. In the initializer list (after the colon), set red
, green
, and blue
to 0
.
Color.black() : red = 0, green = 0, blue = 0;
Factory constructors
#Dart supports factory constructors, which can return subtypes or even null. To create a factory constructor, use the factory
keyword:
class Square extends Shape {}
class Circle extends Shape {}
class Shape {
Shape();
factory Shape.fromTypeName(String typeName) {
if (typeName == 'square') return Square();
if (typeName == 'circle') return Circle();
throw ArgumentError('Unrecognized $typeName');
}
}
Exercise
#Replace the line TODO();
in the factory constructor named IntegerHolder.fromList
to return the following:
- If the list has one value, create an
IntegerSingle
instance using that value. - If the list has two values, create an
IntegerDouble
instance using the values in order. - If the list has three values, create an
IntegerTriple
instance using the values in order. - Otherwise, throw an
Error
.
If you succeed, the console should display Success!
.
Solution for factory constructors example
Inside the factory constructor, check the length of the list, then create and return an IntegerSingle
, IntegerDouble
, or IntegerTriple
as appropriate.
Replace TODO();
with the following code block.
switch (list.length) {
case 1:
return IntegerSingle(list[0]);
case 2:
return IntegerDouble(list[0], list[1]);
case 3:
return IntegerTriple(list[0], list[1], list[2]);
default:
throw ArgumentError("List must between 1 and 3 items. This list was ${list.length} items.");
}
Redirecting constructors
#Sometimes a constructor's only purpose is to redirect to another constructor in the same class. A redirecting constructor's body is empty, with the constructor call appearing after a colon (:
).
class Automobile {
String make;
String model;
int mpg;
// The main constructor for this class.
Automobile(this.make, this.model, this.mpg);
// Delegates to the main constructor.
Automobile.hybrid(String make, String model) : this(make, model, 60);
// Delegates to a named constructor
Automobile.fancyHybrid() : this.hybrid('Futurecar', 'Mark 2');
}
Exercise
#Remember the Color
class from above? Create a named constructor called black
, but rather than manually assigning the properties, redirect it to the default constructor with zeros as the arguments.
Ignore all initial errors in the DartPad.
Solution for redirecting constructors example
Your constructor should redirect to this(0, 0, 0)
.
Color.black() : this(0, 0, 0);
Const constructors
#If your class produces objects that never change, you can make these objects compile-time constants. To do this, define a const
constructor and make sure that all instance variables are final.
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);
final int x;
final int y;
const ImmutablePoint(this.x, this.y);
}
Exercise
#Modify the Recipe
class so its instances can be constants, and create a constant constructor that does the following:
- Has three parameters:
ingredients
,calories
, andmilligramsOfSodium
(in that order). - Uses
this.
syntax to automatically assign the parameter values to the object properties of the same name. - Is constant, with the
const
keyword just beforeRecipe
in the constructor declaration.
Ignore all initial errors in the DartPad.
Solution for const constructors example
To make the constructor const, you'll need to make all the properties final.
class Recipe {
final List<String> ingredients;
final int calories;
final double milligramsOfSodium;
const Recipe(this.ingredients, this.calories, this.milligramsOfSodium);
}
What's next?
#We hope you enjoyed using this tutorial to learn or test your knowledge of some of the most interesting features of the Dart language.
What you can try next includes:
- Try other Dart tutorials.
- Read the Dart language tour.
- Play with DartPad.
- Get the Dart SDK.
Unless stated otherwise, the documentation on this site reflects Dart 3.7.3. Page last updated on 2025-04-21. View source or report an issue.