Learn Dart Before Diving into Flutter: A Dart Tutorial for Android Developers

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.

1

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/

Learn Dart Before Diving into Flutter: A Dart Tutorial for Android Developers

Table of Contents

Learn Dart Before Diving into Flutter: A Dart Tutorial for Android Developers

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 ??.

2

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.

3

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.

4

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.

5

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 &amp;&amp; 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: ?.

6

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.

7

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;
}
8

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
9

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
10

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.

11

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

Leave a Comment