This article is authored by
Author:Sun Qiang Jimmy
Link:
https://www.jianshu.com/p/b6a773ed0fc1
This article is published with the author’s permission.
Introduction
This article is based on the official documentation, summarizing the similarities and unique aspects of the Dart language compared to Java and Kotlin. Therefore, readers should have a basic understanding of Java and Kotlin. By the end of this article, you should be able to understand most of the Flutter code.
While learning Dart, we can use DartPad to write and debug Dart code, experiencing most of the language features of Dart.
https://dartpad.cn/
Table of Contents
Overview
The Dart language draws from both Java and JavaScript. It is very similar to Java in terms of static syntax, such as type definitions, function declarations, and generics, while it resembles JavaScript in terms of dynamic features, such as functional characteristics and asynchronous support.
In addition to integrating the advantages of Java and JavaScript, Dart also features some other expressive syntax, such as optional named parameters, the cascade operator .., the conditional member access operator ?. and the null-aware assignment operator ??.
Variables
2.1 Variable Definition
When creating a variable, we can use regular type definitions:
String name = 'Bob'
We can also use the var keyword to create it:
var name = 'Bob'
In the above example, the type of the variable name will be inferred as String. If you want to allow it to be of any type, you can specify it as dynamic, as shown below:
dynamic name = 'Bob';
name = 1
Uninitialized variables are default initialized to null, even numeric variables.
2.2 final and const
Dart does not have the val keyword like Kotlin, but it has final and const. The difference is that final variables are initialized the first time they are used, while const variables are compile-time constants and must be assigned a value at the time of declaration. Example code:
final name = 'Bob'; // final can directly replace the var keyword
final String nickname = 'Bobby'; // can also precede a specific type
const bar = 1000000; // direct assignment
const double atm = 1.01325 * bar; // can use other const variables for assignment
Another difference is that final variables cannot modify the referenced object, while const variables cannot modify the referenced object or even its properties, making it an immutable object. Example code:
class Person {
var name;
Person(this.name);
}
class ImmutablePoint {
static const ImmutablePoint origin = ImmutablePoint(0, 0);
final num x, y;
const ImmutablePoint(this.x, this.y);
}
void main() {
final person = Person('Bob');
person.name = 'Bobby'; // No problem
const point = ImmutablePoint(1, 1);
point.x = 2; // x cannot be reassigned
}
From the above example, you can see that you can declare a constructor as const, and objects created by such constructors represent immutable objects. If you add const before the constructor, you must ensure that all properties are final or const, as in the ImmutablePoint class in the code above.
When a variable is marked as const, the constructor of the class it references must also be const, meaning that the class is immutable.
Additionally, the const keyword can be placed on both sides of the equal sign, but it is usually omitted on the right side, as it can be inferred from the context starting from Dart 2:
const point = const ImmutablePoint(1, 1); // equivalent to `const point = ImmutablePoint(1, 1);`
Finally, if you use const to modify a member variable in a class, you must also add the static keyword, i.e., static const, such as the origin variable in the ImmutablePoint class.
Built-in Types
3.1 Numeric Types
Dart supports int, double, and num, where int and double are subclasses of num. Here are ways to convert between strings and numbers:
// String -> int
var one = int.parse('1');
// String -> double
var onePointOne = double.parse('1.1');
// int -> String
String oneAsString = 1.toString();
// double -> String
String piAsString = 3.14159.toStringAsFixed(2); // keep 2 decimal places, output 3.14
3.2 Strings
You can create strings using single quotes or double quotes:
var s1 = 'Using single quotes to create string literals.';
var s2 = "Double quotes can also be used to create string literals.";
var s3 = 'When creating a string with single quotes, you can use a backslash to escape strings that conflict with single quotes: \\'.
var s4 = "In double quotes, there's no need to escape strings that conflict with single quotes: '";
You can use the ${expression} format to include expressions in strings, similar to Kotlin, which will not be detailed here.
You can also use the + operator to concatenate two strings, or place multiple strings together to form one:
var s1 = 'You can concatenate'
'strings'
"Even if they are not on the same line.";
You can create multi-line strings using three single quotes or three double quotes:
var s1 = '''
You can create multi-line strings like this.
''';
var s2 = """
This is also a multi-line string.
""";
Adding an r prefix before a string creates a raw string, which means it will not be processed (e.g., escaped):
var s = r'In a raw string, the escaped string \n will directly output "\n" instead of being escaped to a newline.'
3.3 Boolean Types
Dart uses the bool keyword to represent boolean types.
3.4 List
In Dart, arrays are represented by List objects. List literals are the same as array literals in JavaScript, as shown below:
var list = [1, 2, 3];
You can manipulate Dart’s List elements similarly to Java arrays.
If you want to create a compile-time constant List, just add the const keyword before the List literal, as mentioned in section 2.2, example code:
var constantList = const [1, 2, 3];
constantList[1] = 1; // Error, cannot modify the array
Dart introduced the spread operator … and the null-aware spread operator …? in 2.3, providing a concise way to insert multiple elements into a collection.
For example, you can use the spread operator … to insert all elements from one List into another:
var list = [1, 2, 3];
var list2 = [0, ...list];
If the right side of the spread operator might be null, you can use the null-aware spread operator …? to avoid exceptions:
var list;
var list2 = [0, ...?list]; // list2 only contains one element 0
Dart 2.3 also introduced Collection If and Collection For, allowing you to use conditions and loops when constructing collections, example code:
var nav = [
'Home',
'Furniture',
'Plants',
if (promoActive) 'Outlet'
];
var listOfInts = [1, 2, 3];
var listOfStrings = [
'#0',
for (var i in listOfInts) '#$i'
];
3.5 Set
Below is how to create a Set collection using Set literals:
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
The difference between Set literals and List literals is that Set literals use curly braces {}, while List literals use square brackets [].
You can create an empty Set by adding a type parameter before the {}, or assign {} to a variable of type Set:
var names = <String>{}; // Create Set with type + {}
Set<String> names = {}; // Declare variable with type to create Set
You can add the const keyword before a Set literal to create a compile-time constant Set:
final constantSet = const {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
constantSet.add('helium'); // Error, cannot modify the collection
Starting from Dart 2.3, Sets also support using spread operators (… and …?) as well as Collection If and Collection For operations, just like Lists.
3.6 Map
In Dart, you create a Map using Map literals:
var nobleGases = {
2: 'helium',
10: 'neon',
18: 'argon',
};
You can also create a Map using the Map constructor:
var nobleGases = Map();
// Add key-value pairs
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';
You can create an empty Map by adding a type parameter before the {}, or assign {} to a variable of type Map:
var nobleGases = <int, String>{}; // Create Map with type + {}
Map<int, String> nobleGases = {}; // Declare variable with type to create Map
Here’s a question: if I forget to add the type parameter as shown in the code below, is the variable a Set or a Map?
var nobleGases = {};
The answer is actually Map. Because of the existing Map literal syntax, {} defaults to Map type. If you forget to annotate the type on {} or assign it to an undeclared type variable, Dart creates an object of type Map<dynamic, dynamic>.
Like List, adding the const keyword before a Map literal creates a compile-time constant Map:
final constantMap = const {
2: 'helium',
10: 'neon',
18: 'argon',
};
constantMap[2] = 'Helium'; // Error, cannot modify the mapping
Starting from Dart 2.3, Maps also support using spread operators (… and …?) as well as Collection If and Collection For operations, just like Lists.
Functions
Dart is a truly object-oriented language, so functions are also objects and of type Function. Here is an example of defining a function:
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
If the function body contains only one expression, you can use shorthand syntax:
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
The syntax => expression is a shorthand for { return expression; }, and => is also known as the fat arrow syntax.
Note that only expressions can be placed between => and ;, so you cannot put an if statement inside, but you can place a conditional expression.
Functions can have two types of parameters: required parameters and optional parameters. Required parameters are defined at the beginning of the parameter list, while optional parameters are defined afterward. Optional parameters can be named or positional. Let’s detail this.
4.1 Optional Parameters
Optional parameters can be either named or positional, and you can choose to use either in the parameter list, but both cannot appear simultaneously.
4.1.1 Named Parameters
When defining a function, you can use {param1, param2, …} to specify named parameters:
void enableFlags({bool bold, bool hidden}) {...}
When you call this function, you need to specify named parameters in the form of parameterName: parameterValue:
enableFlags(bold: true, hidden: false);
Although named parameters are a type of optional parameter, you can still use the @required annotation to indicate that a named parameter is required, in which case the caller must provide a value for that parameter. For example:
void enableFlags({bool bold, @required bool hidden}) {...}
Named parameters have considerable applications in Flutter.
4.1.2 Positional Parameters
You can wrap a series of parameters in [] to use them as positional parameters:
String say(String from, String msg, [String device]) {...}
say('Bob', 'Howdy') // Call the above function without using optional parameters
say('Bob', 'Howdy', 'smoke signal') // Call the above function using optional parameters
4.1.3 Default Parameter Values
You can define default values for named and positional parameters using =, with the default value being null if no default value is specified. Example code:
void enableFlags({bool bold = false, bool hidden = false}) {...}
Lists or Maps can also be used as default values, example code:
void doStuff(
{List<int> list = const [1, 2, 3],
Map<String, String> gifts = const {
'first': 'paper',
'second': 'cotton',
'third': 'leather'
}}) {
print('list: $list');
print('gifts: $gifts');
}
4.2 main() Function
Every Dart program must have a top-level main() function as the entry point of the program. The main() function returns void and has an optional parameter of type List<String>.
4.3 Functions as Objects
Like Kotlin, you can pass functions as parameters to another function. For example:
void printElement(int element) {
print(element);
}
var list = [1, 2, 3];
// Pass the printElement function as a parameter.
list.forEach(printElement);
You can also assign functions to a variable, such as:
var loudify = (msg) => msg.toUpperCase();
print(loudify('hello'));
4.4 Anonymous Functions (Lambda Expressions)
In Dart, the format for anonymous functions (or Lambda expressions) is as follows:
([[Type] parameter[, …]]) {
function body;
};
The following code defines an anonymous method with only one parameter item and no parameter type:
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
If the function body contains only one statement, you can use the fat arrow shorthand:
list.forEach((item) => print('${list.indexOf(item)}: $item'));
4.5 Return Values
In Dart, all functions have return values; if there is no return statement, the last line of the function defaults to executing return null.
Operators
5.1 Arithmetic Operators
The commonly used arithmetic operators supported by Dart are the same as those in Java, with the only difference being that the result of the division operator / is a floating-point number, while integer division is represented by ~/ . Example code:
print(5 / 2); // Output 2.5
print(5 ~/ 2); // Output 2
5.2 Relational Operators
The relational operators supported by Dart are also the same as those in Java. One similar to Kotlin is that to check if two objects represent the same entity, use == . If it is a custom class, you need to override the == operator and hashCode value.
If you need to determine if two objects are exactly the same, you can use the identical() function. Example code:
class Person {
final String firstName, lastName;
Person(this.firstName, this.lastName);
@override
bool operator ==(dynamic other) {
if (other is! Person) return false;
Person person = other;
return person.firstName == firstName && person.lastName == lastName;
}
@override
int get hashCode {
int result = 17;
result = 37 * result + firstName.hashCode;
result = 37 * result + lastName.hashCode;
return result;
}
}
void main() {
Person person1, person2;
print(person1 == person2); // Both are null, thus equal
person1 = Person("Jimmy", "Sun");
person2 = Person('Jimmy', 'Sun');
print(person1 == person2); // Output true
print(identical(person1, person2)); // Output false
}
5.3 Type Checking Operators
Similar to Kotlin, Dart uses as, is, and is! to check object types, and after using is, Dart can infer that type.
5.4 Assignment Operators
Dart can use = to assign values, and can also use ??= to assign values to variables that are null:
a = value;
b ??= value; // Assigns value only if b is null
5.5 Conditional Expressions
Dart has two special operators that can replace if-else statements, one being the ternary expression: condition ? expression1 : expression2, and the other being similar to Kotlin’s Elvis operator: expression1 ?? expression2, which returns the value of expression1 if it is not null, otherwise returns the value of expression2.
5.6 Cascade Operator (..)
Similar to Kotlin’s scope functions, the cascade operator .. allows you to call multiple variables or methods on the same object consecutively.
querySelector('#confirm') // Get object
..text = 'Confirm' // Use object's member
..classes.add('important')
..onClick.listen((e) => window.alert('Confirmed!'));
The cascade operator can also be nested, for example:
final addressBook = (AddressBookBuilder()
..name = 'jenny'
..email = '[email protected]'
..phone = (PhoneNumberBuilder()
..number = '415-555-0100'
..label = 'home')
.build())
.build();
5.7 Conditional Access Operator
Dart also supports Kotlin’s safe call operator: ?.
Control Flow Statements
6.1 For Loop
In Dart, in addition to supporting standard for loops, classes that implement the Iterable interface, such as List and Set, also support for-in style iteration:
var collection = [0, 1, 2];
for (var x in collection) {
print(x); // 0 1 2
}
6.2 Switch and Case
In Dart, every non-empty case clause must have a break statement, but it can also end a non-empty case statement through continue, throw, or return, otherwise it will throw an error.
However, Dart also supports empty case statements, allowing them to execute in a fall-through manner. Example code:
var command = 'CLOSED';
switch (command) {
case 'CLOSED': // Empty case statement in fall-through form.
case 'NOW_CLOSED':
executeNowClosed(); // This statement will execute for both CLOSED and NOW_CLOSED case condition values.
break;
}
If you want to implement fall-through in a non-empty case statement, you can use continue with a label:
var command = 'CLOSED';
switch (command) {
case 'CLOSED':
executeClosed();
continue nowClosed; // Continue executing the case clause labeled nowClosed.
nowClosed:
case 'NOW_CLOSED':
executeNowClosed(); // This statement will execute for both CLOSED and NOW_CLOSED case condition values.
break;
}
Other control flow statements such as if-else, while, do-while, break, and continue are used the same way as in Java, and will not be detailed here.
Exceptions
Like Kotlin, all exceptions in Dart are unchecked exceptions.
7.1 Throwing Exceptions
In Dart, you can throw any non-null object as an exception, not limited to the Exception or Error class, for example:
throw 'Out of llamas!'
7.2 Catching Exceptions
In Dart, use on or catch to catch exceptions, using on to specify the exception type and catch to catch the exception object; both can be used alone or together. Example code:
try {
throw Exception('Out of llamas!');
} on Exception { // Using on alone
print("error");
}
try {
throw Exception('Out of llamas!');
} catch (e) { // Using catch alone
print(e);
}
try {
throw Exception('Out of llamas!');
} on Exception catch (e) { // Using both on and catch
print(e);
}
You can use the keyword rethrow to throw the caught exception again:
try {
throw Exception('Out of llamas!');
} on Exception {
rethrow;
}
Classes
8.1 Constructors
8.1.1 Member Variable Assignment
Dart provides special syntax sugar to simplify the assignment of member variables:
class Point {
num x, y;
Point(this.x, this.y);
}
8.1.2 Named Constructors
You can declare multiple named constructors for a class to express clearer intentions:
class Point {
num x, y;
Point(this.x, this.y);
// Named constructor
Point.origin() {
x = 0;
y = 0;
}
}
Constructors cannot be inherited, and subclasses cannot inherit named constructors from parent classes.
8.1.3 Calling Parent Class Non-default Constructors
If the parent class does not have a no-argument constructor, the subclass must call one of the parent class’s constructors. To specify a parent class constructor for a subclass constructor, simply use : before the constructor body. Example code:
class Employee extends Person {
final Map data;
Employee(String firstName, String lastName, this.data) : super(firstName, lastName);
}
class Employee extends Person {
final Map data;
Employee(String firstName, String lastName, this.data) : super(firstName, lastName) {
// ...
}
}
8.1.4 Initialization List
Besides calling the parent class constructor, you can also initialize instance variables before the constructor body executes, separating each instance variable with commas. Example code:
class Employee extends Person {
final Map data;
Employee(String firstName, String lastName)
: data = {}, super(firstName, lastName) {
// ...
}
}
8.1.5 Redirecting Constructors
Sometimes, a constructor in a class calls another constructor in the same class; this redirecting constructor has no function body, simply using : after the function signature to specify which other constructor to redirect to:
class Point {
num x, y;
// This class's main constructor.
Point(this.x, this.y);
// Delegates implementation to the main constructor.
Point.alongXAxis(num x) : this(x, 0);
}
8.1.6 Using Constructors
Starting from Dart 2, the new keyword is optional when creating objects.
Additionally, when creating compile-time constants by adding the const keyword before two constructors with the same parameter values, those two objects are the same object:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
print(identical(a, b)); // Output true
8.2 Getting the Type of an Object
You can use the runtimeType property of the Object class to get the type of an object at runtime; the type of the object is an instance of the Type class:
print('The type of a is ${a.runtimeType}');
8.3 Getter and Setter Methods
Each property of an instance object has an implicit Getter method, and if it is not final, it will also have a Setter method. You can also use the get and set keywords to add Getter and Setter methods for additional properties:
class Rectangle {
num left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// Define two computed properties: right and bottom.
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
8.4 Abstract Classes and Implicit Interfaces
Dart does not have an interface keyword, but you can declare an abstract class using the abstract keyword.
In fact, every class implicitly defines an interface and implements that interface, which includes all member variables and methods of that class. If you want to create class A to support calling variables and methods of class B without inheriting from B, you can implement B as an interface. Example code:
// The implicit interface of the Person class includes the greet() method.
class Person {
// The _name variable is also included in the interface, but it is only visible within the library.
final _name;
// The constructor is not included in the interface.
Person(this._name);
// The greet() method is in the interface.
String greet(String who) => 'Hello, $who. I am $_name.';
}
// An implementation of the Person interface.
class Impostor implements Person {
get _name => '';
String greet(String who) => 'Hello $who. Do you know who I am?';
}
8.5 Extension Methods
You can create extension methods using the extension on keyword, example code:
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
print('123'.parseInt() == 123); // Output true
Generics
Dart’s generic types are consistent with Java, with the only difference being that it retains type information at runtime, meaning it is not erased:
var names = List<String>();
names.addAll(['Xiao Yun', 'Xiao Fang', 'Xiao Min']);
print(names is List<String>); // true
Libraries and Visibility
Every Dart program is a code library.
Dart does not have member access modifiers like public, protected, and private as in Java. If an identifier starts with an underscore _, it indicates that the identifier is private within the code library.
Asynchronous Support
11.1 Handling Futures
In Dart, asynchronous programming is implemented using the async and await keywords, which are identical to their usage in JavaScript, making your code appear synchronous. For example, the code below uses await to wait for the result of an asynchronous function:
await lookUpVersion();
await can only be used within an asynchronous function marked with the async keyword:
Future checkVersion() async {
var version = await lookUpVersion();
// Continue processing logic with version
}
Although asynchronous functions can handle time-consuming operations, they do not wait for these operations to complete. An asynchronous function will return a Future object when it encounters the first await expression, and then continue executing after the await expression completes.
You can use try-catch to handle exceptions caused by await:
try {
version = await lookUpVersion();
} catch (e) {
// Reaction when the version cannot be found
}
You can also use the await keyword multiple times within an asynchronous function, example code:
var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
await expressions usually return a Future object; if not, they will automatically be wrapped in a Future object. A Future object represents a