© Frank Zammetti 2019
Frank ZammettiPractical Flutterhttps://doi.org/10.1007/978-1-4842-4972-7_2

2. Hitting the Bullseye with Dart

Frank Zammetti1 
(1)
Pottstown, PA, USA
 

In the last chapter, you got a brief introduction to Dart, the language Google chose to underpin Flutter. It was a cursory introduction, just giving you a high-level overview of (some of) what Dart has to offer, but it was enough, along with some basic code samples, to provide you with a general idea what Dart is all about.

As you can imagine, given that all Flutter apps are built with Dart, it’s something you must have a decent grasp of, and that’s what this chapter is all about! As you read through it, you’ll get to know Dart pretty well, at least well enough to get started with the code in subsequent chapters (where the knowledge from this chapter will hopefully get embedded in that brain of yours well and good). We’ll get a bit more in-depth, but as we do, recall that introductory section from Chapter 1 because it, along with this chapter, forms a complete picture of Dart.

To be clear, this will not be an exhaustive look at Dart. I may fill in some gaps in later chapters as we explore application code, but some topics are either very rarely used or very specialized, and I felt it wouldn’t hurt you in any way to skip them. Indeed, what’s covered here is likely to be 95% of what you’ll ever need to know about Dart. Naturally, the online Dart documentation that you can find at www.dartlang.org , which is Dart’s home on the Web, has all those additional topics covered, plus in some cases expands on what’s in this chapter, so if you really want to deep-dive into Dart, then stop over there when you’re done with this chapter and have at it!

Now, let’s start the journey by talking about some real basics, some key concepts that you must know in order to get far with Dart.

The Things You Must Know

As with any modern programming language, Dart has a lot to offer, but it’s built on a few key concepts that underpin most of it. Some of these are things that Dart has in common with other languages while some of them are things that make it stand out from the pack a bit.

But even before we start talking concepts, wanna see something cool? Take a look at Figure 2-1. This is what’s known as DartPad, and it’s a web app provided by the dartlang.​org web site, more specifically https://dartpad.dartlang.org .
../images/480328_1_En_2_Chapter/480328_1_En_2_Fig1_HTML.jpg
Figure 2-1

FlutterPad, your experimental playground for Dart code on the Web!

This neat tool allows you to play with most of Dart’s capabilities in real-time without having to install any tooling at all! It’s a great way to test concepts out quickly and easily. Just enter some code on the left and click Run, and you’ll see the results on the right. Quick, super-useful and straightforward!

Now, on with the learning!

All languages have keywords, of course, tokens that you can’t use because they have specific meaning in the language you’re using, and Dart is no exception. Let’s examine those keywords now. I’ve tried to group them into related concepts that were applicable to try and give you as much context as possible as we go through these. I’ve also tried to order them in a reasonable way rather than just a purely alphabetical list so that you’ll learn about many of the concepts you need to know to be an effective Dart developer in a logical sequence as we go through them.

Note

This book assumes you aren’t a complete beginner to programming generally and specifically that you already have some experience with a C-like language. That’s especially true in this section because many of these keywords are no different than in any other language you’re familiar with. For those, I’ll offer only very brief descriptions, and I’ll save the more in-depth explanations for those keywords and concepts that are unique to Dart, or if not unique are a little out of the ordinary at least.

No Comment: All About Comments

I want to start with our discussion by talking about comments in Dart because I feel like commenting, in general, is something that not enough developers do and do effectively. Comments are a critical part of programming whether it’s something you enjoy doing or not, and as such, Dart provides three forms of comments.

First, Dart supports single-line comments using the likely familiar // character sequence. The compiler ignores anything following this on the line. As such, // can be the first thing on the line, or you can drop such a comment on the end of a line:
// Define the user's age.
int age = 25; // The age is 25

Now, don’t get me wrong: I’m not suggesting this is an example of good or proper commenting! Quite the opposite in fact! I’m just using it as an example to show this form of a comment in Dart.

The second form is multi-line comments, and again here, Dart is typical by using the /* and */ marker sequences:
/*
  This function calculates account balance
  using the Miller-Hawthorne method
  of value calculation.
*/

Anything between those two sequences is ignored.

The final form of commenting provided by Dart are called documentation comments. These comments are designed to produce useful verbiage when documentation generation tooling is used on Dart code. These can be either single-line or multi-line by using the /// or /** and */ sequences:
/// This is a documentation comment.
/**
  This, too,
  is a
  documentation comment.
*/
As with the other forms, anything on a line with /// (which, again, can be the start of the line or post-fixed to the end of a line) is ignored. However, there is an exception: anything enclosed in brackets in such a comment is taken to be a reference to a class, method, field, top-level variable, function, or parameter, resolved to the lexical scope of the documented program element. So, for example:
class Pet {
  num legs;
  /// Feeds your pet [Treats].
  feed(Treats treat) {
    // Feed the critter!
  }
}

Here, when documentation is generated (which you can do with the Dart SDK’s dartdoc tool) the [Treats] text will become a link to the API documentation for the Treats class (assuming dartdoc can find Treats in the lexical scope of the Pet class).

Tip

This is a bit of a tangent, but one I feel very strongly about and will use my author soapbox to espouse a bit! Please, comment your code and comment it well, most especially if anyone but you is ever going to look at it (but trust me, even if you expect it’ll only ever be you, a well-written comment on code you haven’t looked at in years will be a real godsend). There is an eternal ongoing debate in the programming world about commenting. Some people are completely averse to writing any sort of comments (this is the “self-documenting code” camp), others just want people to write useful comments. I’m very much in the latter camp, and I’m even a bit more extreme about it. To me, comments are just as important as code, and I’ve come to that conclusion based on 25 years of professional software development where maintaining other peoples’ code, or just my own years later, is a huge challenge. Yes, try to write “self-documenting” code for sure, that’s completely great advice. But then, once you’ve done so, comment anyway! Of course, don’t tell me that a++; increments a in a comment, because that’s pointless. You have to write meaningful comments obviously. But, if you aren’t putting as much attention into writing good comments as you are writing good code then, in at least this author’s opinion, you just aren’t doing your job thoroughly and correctly.

Nothing Stays the Same: Variables

To begin with, everything is an object in Dart. Variables in Dart, as in virtually any language, store a value or a reference to something. In some languages, there is a difference between primitives like numbers and string and objects, which are instances of classes. Not so in Dart! Everything here is an object, even simple numbers, functions, and even null are all objects, which are always instances of classes, and all of which extend from a common Object class.

Variable Declaration and Initialization

In Dart, you can declare a variable in two ways:
var x;
...or...
<some specific type> x;
In this case, note that x has a value of null, even if it’s of a numeric type. That’s always the default value if you don’t define the variable which, as in virtually all languages, you can combine with the declaration:
var x = "Mel  Brooks";
String x = "Mel  Brooks";

And there, you see something interesting: when you do var x, Dart will infer the type from the value assigned. It knows that x is a reference to a String in that case. But you can also declare the type explicitly, as in String x, either way works. There is a style guideline that says you should declare local variables using var and others using a type annotation (which is what the String in String x is considered), but that’s a matter of preference ultimately.

Also, there is a third option:
dynamic x = "Mel Books";
Here, the dynamic type annotation tells Dart that what x references can change over time. So, if later you do
x = 42;

...Dart won’t complain that x now points to a numeric value rather than a string.

There is, in fact, a fourth and final option for declaring a variable:
Object x = "Mel Brooks";

Since everything in Dart extends from the common Object class, this works too. But, as mentioned in the bullet points that started this chapter off, there is an important difference. If a variable is of type Object, and you try to call a method on the reference that doesn’t exist, then you’ll get a compile-time error. With dynamic, that won’t be the case, and you’ll only see the problem at runtime.

Constants and Final Values

Finally, related to all of this is the const and final keywords , both of which define a variable as being a constant, a final immutable value:
const x = "Mel Books";
It works with type annotations too:
const String x = "Mel Brooks";
And you can use final instead if you prefer:
final x = "Mel Brooks";
But, it’s not just a preference. The difference is that const variables are constant at compile-time, which means their value can’t depend on anything at runtime. So, if you tried:
const x = DateTime.now();
...that won’t work. But, this will:
final x = DateTime.now();

Essentially, final means you can only set it once, but you can do so at runtime, while const means you can only set it once, but its value must be knowable at compile-time.

One final point on const: you can apply it to values as well as variable. For example (and don’t worry that we haven’t talked about what List is yet, we’ll get to that soon – but I’m pretty sure you can figure it out anyway!):
  List lst = const [ 1, 2, 3];
  print(lst);
  lst = [ 4, 5, 6 ];
  print(lst);
  lst[0] = 999;
  print(lst);

That works as expected: the initial list of values (1, 2, 3) is printed, then a new list is referenced and printed (4, 5, 6), and finally the first element is updated, and the list again printed (999, 5, 6). However, what happens if you move the lst[0] = 999; line before the reassignment of lst on the third line? Well, now you’ll get an exception because you’re trying to alter a list that was marked as const. This is something a bit atypical in Dart (I’m sure some other language has his, but it’s not common certainly).

Note

Variables and other identifiers can start with a letter or an underscore and then be followed by any combination of letters and numbers (and, of course, as many underscores as your heart desires!) Any that start with underscore have a special meaning: it is private to the library (or class) it’s in. Dart doesn’t have visibility keywords like public and private as is found in other languages like Java, but starting with an underscore does much the same thing as private does in Java and other languages.

Everybody Has a Type: Data Types

Dart is a strongly typed language, but curiously, you don’t need to annotate types. They’re optional, and that’s because Dart performs type inference when annotations aren’t present.

String Values

Dart offers a String type, which is a sequence of UTF-16 code units. Strings can be initialized using either single or double quotes. Strings can include expressions using the ${expression} syntax. If the expression refers to an identifier, then you can drop the curly braces. So:
  String s1 = "Rickety Rocket";
  String s2 = "${s1} blast off!";
  String s3 = '$s1 blast off!';
  print (s2);
  print (s3);

You can see double and single quotes here, and you can see both forms of expressions (sometimes referred to as tokens).

String concatenation can use the + operator, as you can in a lot of languages, or it can use adjacent string literals, like this:
return "Skywalker," "Luke";

Those string literals can, of course, include expression tokens as well.

Numeric Values

Your typical integer numeric values have a type of int. The range of values is –263 to 263–1 on the Dart VM (the range will take on the range of JavaScript numbers when Dart code is compiled to JavaScript, something not discussed in this book, and it will never be larger than 64 bits, depending on platform)

A double precision floating point number, as specified by the IEEE 754 standard, has a type of double.

Both int and double are subclasses of num, so you can define a variable as num w = 5; or num x = 5.5; as well as int y = 5; or double z = 5.5; and Dart knows that x is a double based on its value just like it knows z is because you specified it.

A numeric can be turned into a string using the toString() method of the int and double classes:
  int i = 5;
  double d = 5.5;
  String si = i.toString();
  String sd = d.toString();
  print(i);
  print(d);
  print(si);
  print(sd);
And, a string can be turned into a number with the parse() method or the int and double classes:
  String si = "5";
  String sd = "5.5";
  int i = int.parse(si);
  double d = double.parse(sd);
  print(si);
  print(sd);
  print(i);
  print(d);

Note

It’s a little weird to my eyes but notice that String is the only type that starts with a capital letter. I’m not honestly sure why that is, but it’s worth pointing out. Well, it’s also kinda/sorta not entirely true: Map and List also start with capitals, as you’ll see a few sections from now. Still, I’m not sure those should be put in the same category as String, given that String is, to mind anyway, a more “intrinsic” data type, like int and double. But we can debate that another time – just realize that some data types start with a capital letter and some don’t!

Boolean Value

Boolean values are of type bool, and only two objects have boolean values: the keywords true and false (which are compile-time constants).

Note that Dart’s type safety means that you can’t write code like this:
if (some_non_boolean_variable)
Instead, you must write something like:
if (some_non_boolean_variable.someMethod())

In other words, the evaluation of a logic statement can’t be “truthy” like you can do in some languages. In Dart, it must always evaluate to one of these bool values.

Lists and Maps

The List class in Dart is akin to an array in most languages. An instance of one is a list of values which are defined with syntax identical to JavaScript:
List lst = [ 1, 2, 3 ];

Note

Generally, you would write list (and later, set and map) when referring to an instance of the Map, Set, or List classes, and you only capitalize them when referring to the actual class.

You, of course, could also do either of these:
var lst1 = [ 1, 2, 3 ];
Object lst2 = [ 1, 2, 3 ];
A list uses a zero-based indexing scheme, so list.length-1 gives you the index of the last element. You can access elements by index:
print (lst[1]);
A list, being an object, has several methods available on it. I’m not going to go over all of them since this chapter isn’t trying to be a reference guide, and especially since most of them can be found in virtually any other language that offers a list-like construct and so you’re likely familiar with most of them already, but here’s a quick example of a few of them:
List lst = [ 8, 3, 12 ];
lst.add(4);
lst.sort((a, b) => a.compareTo(b));
lst.removeLast();
print(lst.indexOf(4));
print(lst);
Dart also offers a Set class , which is similar to List, but it’s an unordered list, which means you can’t retrieve elements by index, you have to use methods contains() and containsAll() instead:
Set cookies = Set();
cookies.addAll([ "oatmeal", "chocolate", "rainbow" ]);
cookies.add("oatmeal"); // No harm, no foul
cookies.remove("chocolate");
print(cookies);
print(cookies.contains("oatmeal"));
print(cookies.containsAll([ "chocolate", "rainbow" ]));

The call to contains() returns true, while the call to containsAll() returns false since chocolate was remove()’d. Note that add()’ing a value that’s already in the set does no harm.

Dart also has a Map class , sometimes called a dictionary or a hash , or an object literal in JavaScript, an instance of which can be created a few ways:
var actors = {
  "Ryan Reynolds" : "Deadpool",
  "Hugh Jackman" : "Wolverine"
};
print(actors);
var actresses = Map();
actresses["scarlett johansson"] = "Black Widow";
actresses["Zoe Saldana"] = "Gamora";
print (actresses);
var movies = Map<String, int>();
movies["Iron Man"] = 3;
movies["Thor"] = 3;
print(movies);
print(actors["Ryan Reynolds"]);
print(actresses["Elizabeth Olsen"]);
movies.remove("Thor");
print(movies);
print(actors.keys);
print(actresses.values);
Map sequels = { };
print(sequels.isEmpty);
sequels["The Winter Soldier"] = 2;
sequels["Civil War"] = 3;
sequels.forEach((k, v) {
  print(k + " sequel #" + v.toString());
});
The first actors map is created using braces and with data defined immediately within it. The second actresses map uses the new keyword to create a new Map instance explicitly. Elements are added to it using bracket notation where the value inside the bracket is the key and the value after the equals is the value to map to that key. The third version shows that you can also define types for the keys and values in a map. That way, if you try to do:
Movies[3] = "Iron Man";

...you will get a compile error because 3 is an int, but the type of the key is defined as String (and likewise, the value type is defined as int, but we’re trying to insert a String).

After that, you can see a few critical methods being used. The remove() method removes an element from a map. You can get a list of the keys and values by reading the keys and values attributes (which really means calling a getter method, as you’ll see later in the section on classes, even though there’s no parenthesis like normally after a method call). The isEmpty() method tells you whether the map is empty or not (there’s also an isNotEmpty() method if you prefer that). Although not shown, a map also provides the contains() and containsAll() methods , just like a list does. Finally, the forEach() method allows you to execute an arbitrary function for each element in the map (the function you supply is passed the key and the value – and there’s more to come on functions, so don’t worry about the details just yet).

As with lists, there are many more utility methods available on maps, too many to go over here, but we’ll likely encounter others as we look at the code of the projects in later chapter.

Finally, one last point related to data types is that there is also a special dynamic type that, in effect, turns off Dart’s type system. Imagine if you write:
Object obj = some_object;
Dart knows that you can call some methods on obj like toString() and hashCode() because they are defined by the Object class that all objects extend from. If you try to call obj.fakeMethod(), then you’ll get a warning because Dart can see, at compile-time, that fakeMethod() isn’t a method of the Object class, or (presumably) of the class that some_object is an instance of. But if you write
dynamic obj = some_object;

Now, if you write obj.fakeMethod() , you won’t get a warning at compile-time, though you will now get an error at runtime. Think of dynamic as a way of telling Dart: “hey, I’m the one in charge here, trust me, I know what I’m doing!”. The dynamic type is typically used with things like return values from interop activities, so you may not encounter it all that much, but it’s worth nothing, and it’s worth understanding that it’s fundamentally different from declaring something of type Object.

When a Single Value Just Won’t Do: Enumerations

Need to have an object that contains a fixed number of constant values? Don’t wanna have a bunch of variable floating around and don’t need a full-blown class? Then an enum (short for enumeration) is right for you! Look! Here comes one now!
enum SciFiShows { Babylon_5, Stargate_SG1, Star_Trek };
And, here’s some things you can do with one:
main() {
  assert(SciFiShows.Babylon_5.index == 0);
  assert(SciFiShows.Stargate_SG1.index == 1);
  assert(SciFiShows.Star_Trek.index == 2);
  print(SciFiShows.values);
  print(SciFiShows.Stargate_SG1.index);
  var show = SciFiShows.Babylon_5;
  switch (show) {
    case SciFiShows.Babylon_5: print("B5"); break;
    case SciFiShows.Stargate_SG1: print("SG1"); break;
    case SciFiShows.Star_Trek: print("ST"); break;
  }
}

Every value in the enum has an implicit index getter method, so you can always find the index of a given value (and you’ll get a compile error if the value isn’t valid in the enum. You can also get a list of all the values in the enum through the values property (which too also has an implicit getter). Finally, enum’s are especially useful in switch statements, and Dart will give you a compile error if you don’t have a case for all the values in the enum.

What’s Your Type: The “as” and “is” Keywords

These two conceptually go together: the is keyword allows you to determine if a reference is of a given type (if it implements a given interface essentially) and as allows you to treat a given type reference as another, assuming it’s a superclass. For example:
if (shape is Circle) {
  print(circle.circumference);
}

This will only print() (which writes content to the console) the circumference if the object reference by shape is of type Circle.

By contrast, you can use as like so:
(shape as Circle).circumference = 20;

That way, if shape is a Circle, it works as expected, and if shape can be cast to a Circle, then I would work too (perhaps shape is of type Oval, which is a subclass of Circle, for example).

Note, however, that in the example of is, nothing will happen if shape isn’t a Circle, but in the as example, an exception will be thrown if shape can’t be cast to Circle.

Going with the Flow: Flow Control (and Logic!) Constructs

Dart has several logic and flow control statements and constructs, most of which will be familiar to someone with any programming experience at all.

Looping

Looping in Dart takes on the familiar for, do, and while loop forms:
for (var i = 0; i < 10; i++) {
  print(i);
}
There is also a for-in form, if the target class is iterable:
List starfleet = [ "1701", "1234", "1017", "2610", "7410"  ];
main() {
  for (var shipNum in starfleet) {
    print("NCC-" + shipNum);
  }
}
A List is one such iterable class, so this works well. If you prefer a more functional style, you can use the forEach form :
main() {
  starfleet.forEach((shipNum) => print("NCC-" + shipNum));
}

Note

Don’t get hung up on these functions, especially if the syntax looks a little foreign to you. We’ll get into functions in just a few sections, and it should all come into focus quickly when we do.

The do and while loops offer the typical two forms, do-while and while-do:
while (!isDone()) {
  // Do something
}
do {
  showStatus();
} while (!processDone());

Note that as in most other languages, the continue keyword is also available in Dart to skip to the next iteration of a loop construct. There is also a break keyword to exit from a loop early (which does double duty in the switch construct too).

Switch

Dart also offers a switch structure, and four keywords work together like in most languages to construct switch statements :
switch (someVariable) {
  case 1:
    // Do something
  break;
  case 2:
    // Do something else
  break;
  default:
    // It wasn't 1 or 2
  break;
}

The switch statement in Dart can deal with integer or string types, and the compared objects must be of the same types (and no subclasses allowed here!), and the classes must not override the == operator.

If Statements

Finally, because they are essentially flow control elements, yes, your all-time favorite logic statement is present in Dart, and it wouldn’t be much use without, would it? Note that in Dart, condition expressions must always evaluate to a boolean value, nothing else is allowed. And yes, you can write else if of course:
if (mercury == true || venus == true ||
  earth == true || mars == true
) {
  print ("It's an inner planet");
} else if (jupiter || saturn || uranus || neptune) {
  print ("It's an outer planet");
} else {
  print("Poor Pluto, you are NOT a planet");
}

Note that if mercury, venus, earth, and mars were bool types then if (mercury || venus || earth || mars) would also be valid here .

The Big Nothing: void

In most languages, if a function doesn’t return anything, you have to slap void in front of it. In Dart, which supports the void keyword , you can do that, but you don’t have to.

In Dart though, void is a bit more... curious.

First, if a function doesn’t explicitly return anything, then you can omit a return type entirely; you don’t even need to put void in front of it like most languages (although you are free to do so if you prefer). In such cases, an implicit return null; is added to the end of the function. This is the case for all the code samples thus far.

If you do put void in front of a function though, you will then get a compile-time error if you try to return anything. That makes sense. But if you try and return null, that’s okay, no error. You can also return a void function (a function that has void before it).

Here’s where it gets a little weird though:
void func() { }
class MyClass {
  void sayHi() {
    print("Hi");
    dynamic a = 1;
    return a;
  }
}
main() {
  MyClass mc = MyClass();
  var b = mc.sayHi();
  print(b);
}

Given that sayHi() is a void function, you’d expect that return a from it would produce an error, right? Well, not so! It will compile. Well, it would compile, except for the print(b); line. That will cause a compile-time error, and the reason is that Dart won’t let you use anything returned from a void function (even though you can capture it, since the var b = mc.sayHi(); line compiles and executes without issue – Dart is kind of a tease that way!).

So yeah, void is kind of a weird thing in Dart. My advice is to not use it unless you specifically know that you need to.

But, void isn’t just for return types. You can also use void in generic type parameters, where they are treated semantically like Object is:
main() {
  List<void> l = [ 1, 2, 3 ]; // Equivalent to List<Object> = [ 1, 2, 3 ];
  print(l);
}

Why you might do this is something I’ll touch upon on the section on asynchronous code.

Smooth Operators

Dart has a robust set of operators for you to work with, most of which are likely familiar to you, as shown in Table 2-1.
Table 2-1

Dart operators

Operator

Meaning

+

Add

-

Subtract

-expr

Prefix unary minus (a.k.a. negation/reverse sign of expression)

*

Multiply

/

Divide

~/

Divide, returning an integer result

%

Get the remainder of an integer division (modulo)

++var

Prefix increment, equivalent to var = var + 1 (expression value is var  + 1)

var++

Postfix increment, equivalent to var = var + 1 (expression value is var)

--var

Prefix decrement, equivalent to var = var – 1 (expression value is var – 1)

var--

Postfix decrement, var = var – 1 (expression value is var)

==

Equal

!=

Not equal

>

Greater than

<

Less than

>=

Greater than or equal to

<=

Less than or equal to

=

Assignment

&

Logical AND

|

Logical OR

^

Logical XOR

~expr

Unary bitwise complement (0s become 1s; 1s become 0s)

<<

Shift left

>>

Shift right

a ? b : c

Ternary conditional expression

a ?? b

Binary conditional expression: if a is not null, return a, otherwise return b

..

Cascade notation

()

Function application

[]

List access

.

Member access

A note on the == operator: This is a value check, not an object check. When you need to test if two variables reference the exact same object, use the identical() global function.

When using the == operator, as in if (a == b), true is returned if they are both null, false if only one is. When this expression is executed, the ==() method of the first operand (yes, == is indeed the name of a method!) is executed.

So:
if (a == b)
...is equivalent to...
if (a.==(b))

A note on the = operator: There is also a ??= operator which does the assignment only if the operand is null.

Another note on the = operator: There are a host of compound operators that combine an assignment and an operation. These are
-=  /=  %=  >>=  ^=  +=  *=  ~/=  <<=  &=  |=

A note on the . operator: There is also a conditional version written ?. that allows you to access a member of something where that something could be null.

Take this code, for example:
var person = findPerson("Frank Zammetti");

If person could be null , then writing print(person?.age) will avoid a null pointer error. The result, in this case, would be null printed, but no error, which is the key point.

A note on the .. operator: This allows you to take code like this:
var person = findPerson("Frank Zammetti");
obj.age = 46;
obj.gender = "male";
obj.save();
...and instead write it like this...
findPerson("Frank Zammetti")
  ..age = 46
  ..gender = "male"
  ..save();

Use whichever style is more pleasing to your eyes, Dart doesn’t care either way.

Classes can also define custom operators , but that statement doesn’t have much value unless we first talk about what classes are all about, so let’s do that now, shall we?

Classing the Joint Up: Object Orientation in Dart

Dart is object-oriented, which means we’re dealing with classes and objects. Defining a class is as simple as
class Hero { }

Yep, that’s it!

Instance Variables

Now, classes frequently have instance variables (or members, or fields, or properties, all are synonymous) like so:
class Hero {
  String firstName;
  String lastName;
}

Any instance variable that you don’t initialize with a value begins with a value of null. Dart will automatically generate a getter (accessor) method for each variable, and it will also generate a setter (mutator) for any non-final variables.

Instance variables can be marked as static as well, which means you can use them without instantiating the class:
class MyClass {
  static String greeting = "Hi";
}
main() {
  print(MyClass.greeting);
}

That will print “Hi”, all without ever creating an instance of MyClass.

Methods

Classes can also have member functions, called methods:
class Hero {
  String firstName;
  String lastName;
  String sayName() {
    return "$lastName, $firstName";
  }
}

We’re going to look at functions in more detail in the next section, but I’m betting you’re already familiar with them generally. If you aren’t, then this book probably isn’t a good starting point for you since it assumes some level of modern programming experience. Right now, understand that the return keywords returns a value from the function (or method, when its part of a class) to the caller.

Now, we have a sayName() method that we could call like so:
main() {
  Hero h = new Hero ();
  h.firstName = "Luke";
  h.lastName = "Skywalker";
  print(h.sayName());
}

That also demonstrates that setter methods have indeed been created for us, which is why h.firstName = "Luke"; works.

I skipped over something there: as in virtually all object-oriented languages, the new keyword instantiates objects of a given type, as seen in the previous code. However, in Dart, the new keyword is optional. So, in addition to the previous code, you can also write
var h = Hero();

To be honest, this was, to my brain, one of the weirdest things to get used to about Dart! I’m not sure there’s any compelling reason to do one vs. the other, so pretty much just write it the way makes the most sense to you!

Methods can also be marked as static, just like instance variables can be:
class MyClass {
  static sayHi() {
    print("Hi");
  }
}
main() {
  MyClass.sayHi();
}

As with the static variable example, this again prints “Hi”, but this time as a result of calling sayHi() without instantiating MyClass first.

Constructors

Now, classes also frequently have constructors, that is, special function that execute when an instance of them is created. Adding one is simple:
class Hero {
  String firstName;
  String lastName;
  Hero(String fn, String ln) {
    firstName = fn;
    lastName = ln;
  }
  String sayName() {
    return "$lastName, $firstName";
  }
}
The constructor always has the same name as the class and doesn’t have a return type annotation. Now, our test code would look like this:
main() {
  Hero h = new Hero("Luke", "Skywalker");
  print(h.sayName());
}
However, because a constructor that just sets instance variable values is such a common pattern, Dart has a shortcut constructor form for this:
class Hero {
  String firstName;
  String lastName;
  Hero(this.firstName, this.lastName);
  String sayName() {
    return "$lastName, $firstName";
  }
}

The “this” Reference

The this keyword references the current instance of the class a block of code is executing within (which a construct or not). Typically, you should only use this when there is a naming conflict. For example:
class Account {
  int balance;
  Account(int balance) {
    this.balance = balance;
  }
}

But, philosophical debates about whether you should ever “mask” variable names like this aside (my own personal style says you never do that, I would name that balance argument inBalance or something different than the class-level balance), this allows you to disambiguate such a case and it’s necessary specifically in this shortcut constructor form.

Note that if your class doesn’t define a constructor, as in the first three versions of Hero mentioned earlier, Dart will generate a default no-argument constructor that just invokes the no-argument constructor of the superclass (which here is Object implicitly). Also, note that subclasses do not inherit constructors.

A constructor can also be marked with the factory keyword. This is used when the constructor might not return an instance of its class. I know, that probably sounds weird because it’s an unusual capability of most OOP languages, but it can happen if, for example, you want to return an existing instance of the class from a cache of already constructed objects and not produce a new object, which is what happens by default. A factory constructor might also return an instance of a subclass rather than the class itself. A factory constructor otherwise works just like any other constructor, and you call them the same too, with the only real difference being that they don’t have access to the this reference.

Subclassing

I mentioned subclasses here a moment ago, so how do we define those? Well, it’s easy:
 class Hero {
  String firstName;
  String lastName;
  Hero.build(this.firstName, this.lastName);
  String sayName() {
    return "$lastName, $firstName";
  }
}
class UltimateHero extends Hero {
  UltimateHero(fn, ln) : super.build(fn, ln);
  String sayName() {
    return "Jedi $lastName, $firstName";
  }
}

The extends keyword , followed by the name of the class we want to subclass, is all it takes.

However, there’s a bit more going on here of interest. First is the notion of named constructors. Gaze in awe at that Hero class. See that Hero.build() method? Well, that’s a constructor too, but it’s what is termed a named constructor. The reason this is necessary is because in the UltimateHero class , due to constructors not being inherited, we need to supply one. But, given that it should do the same as what Hero.build() does, there’s no point in repeating the code (the DRY – Don’t Repeat Yourself – principle). So, how do you call the constructor in the parent class? That’s where the : super.build(fn, ln); bit following the UltimateHero(fn, ln) constructor comes in. The super keyword allows you to call methods or access variables in the parent class. But, there’s no way to call the constructor without it being named. In other words, super(fn, ln), which would work in many other languages, doesn’t in Dart. But, what we can do is call a named constructor without issue, so that’s exactly what we do here, using the syntax from the colon on.

Getters and Setters

Now that you’ve seen all of that, I want to go back to the notion of getters and setters. You see, you can create your own, aside from the ones implicitly created, to in a sense create new instance variables on-the-fly. For that, Dart offers the get and set keywords:
class Hero {
  String firstName;
  String lastName;
  String get fullName => "$lastName, $firstName";
  set fullName(n) => firstName = n;
  Hero(String fn, String ln) {
    firstName = fn;
    lastName = ln;
  }
  String sayName() {
    return "$lastName, $firstName";
  }
}
Here, we now have a fullName field. When we try to access it, we’ll get the same sort of concatenation of lastName and firstName as sayName() provides, but when we try to set it, we’ll be overwriting the firstName field. So, now we can test it:
main() {
  Hero h = new Hero("Luke", "Skywalker");
  print(h.sayName());
  print(h.fullName);
  h.fullName = "Anakin";
  print(h.fullName);
}
The output here will be
Skywalker, Luke
Skywalker, Luke
Skywalker, Anakin

Hopefully you can see why!

Interfaces

Dart doesn’t distinguish the notion of classes and interfaces like most other object-oriented languages do. Instead, a Dart class also implicitly defines an interface. Therefore, we could re-implement the UltimateHero class like so:
class UltimateHero implements Hero {
  @override
  String firstName;
  @override
  String lastName;
  UltimateHero(this.firstName, this.lastName);
  String sayName() {
    return "Jedi $lastName, $firstName";
  }
}

The @override here is a metadata annotation, but we’ll get to those later. For now, just understand that it’s necessary to indicate to dart that we are overriding the superclass’s getter and setter for the two marked fields and without them, we’ll get an error. With that change, we also need to change the constructor because now we’re not extending the class and so don’t have access to the Hero.build() constructor (because constructors are never inherited and also implementing an interface means we don’t have access to the behaviors of the class that provides the interface, we’re just saying that our new class provides that same functionality as contractually obligated by the interface), so it becomes a constructor that mimics what’s in Hero instead. The only other change is swapping the implements keyword in for extends since now we’re implementing the interface defined by the Hero class rather than extending it.

Tip

Why implements vs. extends you ask? It’s a question many ask in the OOP world. Some people feel that a compositional model, which is what implements... err... implements... is cleaner. Others think that hierarchies of classes is more proper classical OOP and so prefer extends. Whatever your view, understand one key point: they aren’t equivalent concepts, and in Dart, as in Java and many other OOP languages, you can only extend a single class directly, but you can implement as many interfaces as you wish. So, if your goal is to build a class that provides an API that mimics multiple classes, then implements is what you likely want. Otherwise, you may be on the extends road instead.

Abstract Classes

Next, let’s quickly touch on abstract. This keyword marks an abstract class, like so:
abstract class MyAbstractClass {
  someMethod();
}
Here, MyAbstractClass can’t be instantiated and instead must be extended by a concrete class that itself can be instantiated. Methods inside abstract classes can provide an implementation, or they can be themselves be abstract, in which case, they must always be implemented by a subclass. Here, someMethod() is considered abstract (because it has no method body), but instead you could also do
abstract class MyAbstractClass {
  someMethod() {
    // Do something
  }
}

In that case, someMethod() has a default implementation and a subclass therefore does not need to provide one if it doesn’t want to.

In addition to extending classes, implementing interfaces and abstract classes, Dart also offers the notion of mixins, which is where the with keyword comes into play:
class Person { }
mixin Avenger {
  bool wieldsMjolnir = false;
  bool hasArmor = false;
  bool canShrink = true;
  whichAvenger() {
    if (wieldsMjolnir) {
      print("I'm Thor");
    } else if (hasArmor) {
      print("I'm Iron Man");
    } else {
      print("I'm Ant Man");
    }
  }
}
class Superhero extends Person with Avenger { }
main() {
  Superhero s = new Superhero();
  s.whichAvenger();
}

Here, we’ve got two classes, Person and Superhero, and one mixin, Avenger (which we know based on the mixin keyword that comes before its definition). Notice that Person and Superhero are empty classes, which means that the call to whichAvenger() must be coming from elsewhere, and it is: we’ve “mixed the Avenger mixin into the Superhero class,” so to speak, by specifying with Avenger in the Superhero class definition. Now, whatever is in the Avenger mixin will also be present in Superhero, and our test code works as expected.

Visibility

In Java and many other OOP languages, you typically need to specify what access code has to class members using keywords like public, private, and protected. Dart is different: everything is public unless it begins with an underscore, which marks it as being private to its library, or class.

Operators

As Steve Jobs used to say: “One more thing!”

Of the various operators Dart provides, the following are special (the commas and the period are not operators!): <, >, <=, >=, -, +, /, ~/, *, %, |, ^, &, <<, >>, [], []=, ~, ==. How are they special? Well, they’re the only ones you can override in a class using the operator keyword:
class MyNumber {
  num val;
  num operator + (num n) => val * n;
  MyNumber(v) { this.val = v; }
}
main() {
  MyNumber mn = MyNumber(5);
  print(mn + 2);
}

Here, the MyNumber class overrides the + operator. The current value of an instance of this class will be multiplied by a value rather than be added together thanks to the function supplied for the + operator in the override. So, when main() executes, rather than printing 7 as you would normally expect from the + operator, it instead prints 10 since it multiplies the value of mn, 5, by the 2 after the overridden + operator in the print() statement.

The only catch is that if you override the == operator, then you should also override the class’s hashCode getter. Otherwise, equivalency can’t reliably be determined.

Whew, that was a lot! But it covers probably the majority of what you’ll need to know about classes and objects in Dart.

Getting Funky with Functions

In Dart, functions are first-class citizens and have their own type: Function. What that means is that functions can be assigned to variables, can be passed as parameters, and can also be stand-alone entities. There’s one key stand-alone function you’re already aware of, and that’s main().

Functions in Dart have some sweet syntactic sugar too (see what I did there?). They can have named parameters, and they can also have optional parameters. You can have optional parameters whether you use named parameters or purely positional (the typical style of parameter list), but you can’t mix the two styles. You can also have default values for parameters. Examine this code:
greet(String name) {
  print("Hello, $name");
}
class MyClass {
  greetAgain({ Function f, String n = "human" }) {
    f(n);
  }
}
main() {
  MyClass mc = new MyClass();
  greet("Frank");
  mc.greetAgain( f : greet, n : "Traci" );
  mc.greetAgain( f : greet);
}

Here, you can see most of that at work. First, we have a stand-alone greet() function . Then, we have a class with a greetAgain() method . This method accepts a named parameter list, and look, one of those parameters is a Function! Also, see how the n parameter has a default value of human defined? Cool, right? Then, inside the function, we call the function referenced by f, passing it n. In other words, whatever function is passed in as the value of the f parameter, because it’s annotated as a Function, we can use that f reference to call it.

Now, in the main() function , we first just call greet(), passing it the name that was passed into it, to have the computer say hello. Then, we call that greetAgain() method of the MyClass instance mc, and this time, we’re passing named parameters, and the value of the f parameter is a reference to the greet() function. I show this twice so you can see how it works if you don’t pass a name, and it’ll just greet a generic human.

Note

In many languages, the data you pass to functions are called arguments. That’s the term I grew up with frankly, but the Dart language docs seem to prefer parameters. Truthfully, I may mix those terms sometimes, but they mean the same in this context.

Unfortunately, DartPad does not, as of this writing, allow for importing libraries, which we would need to use the @required annotation that ideally would be before the n parameter in greetAgain(), but not the f parameter. So, because you may want to plug this code in to DartPad and try it, I left that annotation out. Also note that when using positional parameters, you don’t use @required, you instead wrap the optional parameters in square brackets.

While most functions have a name, they don’t have to, they can also be anonymous. As an example:
main() {
  var bands = [ "Dream Theater", "Kamelot", "Periphery" ];
  bands.forEach((band) {
    print("${bands.indexOf(band)}: $band");
  });
}

Here, there’s a function passed to the forEach() method of the List object bands, but it has no name and as a result, it only exists for the lifetime of the execution of forEach().

An important thing about functions is the scope they introduce. Dart is considered a “lexically scoped” language, which means that the scope of a given thing, a variable mostly, is determined by the structure of the code itself. If it’s enclosed in curly braces, then it’s within that scope, and that scope extends downward, meaning that if you have nested functions, for example (which is something else you can totally do in Dart!), then the deeper in the nesting you go, those elements still have access to everything above. To demonstrate this, check out this example:
bool topLevel = true;
main() {
  var insideMain = true;
  myFunction() {
    var insideFunction = true;
    nestedFunction() {
      var insideNestedFunction = true;
      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}

The nestedFunction() can use any variable all the way up to the top level.

Dart also supports the concept of closures with functions so that a function will capture, or “close around,” its lexical scope, even if the function is used outside of its original scope. In other words, if a function has access to a variable, then the function will, in a sense, “remember” that variable even if the scope the variable is in is no longer “alive”, so to speak, when the function executes.

By way of example:
remember(int inNumber) {
  return () => print(inNumber);
}
main() {
  var jenny = remember(8675309);
  jenny();
}

Here, the call to jenny() print 8675309, even though it wasn’t passed to it. This happens because jenny() includes the lexical scope of remember(), and the execution context at the time the reference is captured, which includes the value passed into the call to remember() when getting the reference. It’s confusing if you’ve never encountered it before, I know, but the good news is that you probably won’t need to use closures very much in Dart in my experience (as compared to, say JavaScript, where it comes up all the time).

Dart also supports arrow, or lambda, notation for defining functions. So, these are equivalent:
talk1() { print("abc"); }
talk2() => print("abc");

Tell Me Is It So: Assertions

The assert keyword is like in most other languages and isn’t used in production builds. It’s used to disrupt normal flow if a given boolean condition is false and throws an AssertionException in that case. For example:
assert (firstName == null);
assert (age > 25);
Optionally, you can attach a message to the assert like so:
assert (firstName != null, "First name was null");

Out of Time: Asynchrony

Asychronous (or just async) programming is big business these days! It’s everywhere, in all languages, and Dart is no exception. In Dart, two classes are key to asynchrony, Future and Stream, along with two keywords, async and await. Both classes are objects that async functions return when a long-running operation begins, but before it completes, allowing the program to await the result while continuing to do other things, then continue where it left off when the result comes back.

To call a function that returns a Future, you use the await keyword:
await someLongRunningFunction();
That’s all you have to do! Your code will pause at this line until someLongRunningFunction() completes. The program can still do other things rather than being blocked by the long-running operation (for example, if an event handler for a button click fires, which would be blocked if someLongRunningFunction() was synchronous). The async function itself must be marked with the async keyword in front of its body definition and must return a Future:
Future someLongRunningFunction() async {
  // Do something that takes a long time
}
There’s one more key piece of information: the function that calls someLongRunningFunction() must itself be marked as async:
MyFunction() async {
  await someLongRunningFunction();
}

You can await a function, whether the same or others, as many times as you wish in a single async function and execution will pause on each.

Note

There is also a Future API that allows you to do much the same thing but without using async and await. I’m not covering this just because async/await is generally considered by most to be a more elegant approach, and I certainly echo that sentiment. Feel free to explore that API on your own though if you’re curious.

Streams are handled in much the same way, but to read data from the Stream you must use an async for loop:
await for (varOrType identifier in expression) {
  // Executes each time the stream emits a value.
}

The difference between the two is simply that using a Future means that the return from the long-running function won’t occur until that function completes, however long it takes. With a Stream, the function can return data little by little over time, and your code that is awaiting it will execute any time the Stream emits a value. You can break or return from the loop to stop reading form the Stream, and the loop will end when the async function closes the Stream. As before, an await for loop is only allowed inside of an async function.

Ssshhh, Be Quiet: Libraries (and Visibility)

Libraries are used in Dart to make code modular and shareable. A library provides some external API to other code that uses it. It also serves as a method of isolation in that any identifier in a library that starts with an underscore character is visible only inside that library. Interestingly, every single Dart app is automatically a library whether you do anything special or not! Libraries can be packaged up and delivered to others using the Dart SDK’s pub tool, which is the package and asset manager

Note

I won’t be covering the creation of libraries here as it’s a bit more advanced and not something we’ll need in this book. So, if you’re interested in distributing your libraries, then you’ll need to consult the Dart documentation. Note that the Dart SDK comes as part of the Flutter SDK, so you have this already.

To use a library, the import keyword comes into play:
import "dart:html";

Some libraries are provided by your own code, and others are built into Dart, as is this one. For those built-in libraries, the form of the URI, which is what the part of the statement in quotes is, has a particular form: it begins with dart:, which is the scheme identifier portion of the URI.

If the library that you’re importing comes from a package, which was touched on briefly in Chapter 1 and which we’ll get into in more detail starting with the next chapter, then instead of dart: you would use the package: scheme:
import "package:someLib.dart";
If the library is part of your code, or perhaps something you copied into your codebase, then the URI is a relative file system path:
import "../libs/myLibrary.dart";
Sometimes, you may want to import two libraries, but they have conflicting identifiers in them. For example, maybe lib1 has an Account class, and so does lib2, but you need to import both. In that case, the as keyword comes into play:
import "libs/lib1.dart";
import "libs/lib2.dart" as lib2;
Now, if you want to reference the Account class in lib1, you do
Account a = new Account();
But if you want the Account object from lib2, you would write
lib2.Account = new lib2.Account();
With the imports shown here, everything in the library would be imported. You don’t have to do that though; you can import just parts of a library too:
import "package:lib1.dart" show Account;
import "package:lib2.dart" hide Account;

Here, only the Account class from lib1 would be imported, and everything except the Account class from lib2 would be imported.

So far, all the imports shown would import the library immediately. But, you can also defer that loading, which helps reduce your app’s initial startup time:
import "libs/lib1.dart" deferred as lib1;
Now, there’s a little more work to be done on your part! When you get to the point in your code where you need that library, you must then load it:
await lib1.loadLibrary();

As you learned in the last section, that code must be in a function marked async.

Note that invoking loadLibrary() on a library multiple times is fine, no harm is done. Also, until the library is loaded, the constants defined in the library, if any, aren’t actually constants – they don’t exist until it’s loaded, so you can’t use them.

Deferred loading can also be handy if you want to do A/B testing with your app because you can dynamically load one library vs. another to test the differences.

Let’s Be Exceptional: Exception Handling

Exception handling in Dart is simple and looks a lot like Java or JavaScript, or indeed most other languages that deals with exceptions. In contrast to many other languages though, in Dart, you are not required to declare what exceptions a given function must throw, nor must you catch any. In other words, all exceptions in Dart are unchecked.

To begin, you can throw an exception yourself:
throw FormatException("This value isn't in the right format");

Exceptions are objects, so you need to construct one to throw.

An interesting thing about Dart is that you don’t need to throw any specific object or even an object of a particular subtype. You can throw anything as an exception:
throw "This value isn't in the right format";

That’s throwing a string as an exception, and that’s perfectly fine in Dart. However, with that said, it’s generally considered bad form to throw anything that doesn’t extend from the Error or Exception classes that Dart provides, so this “throw anything” is one capability you might want just to forget exists in Dart!

On the flip side, to catch an exception, you write
try {
  somethingThatMightThrowAnException();
} on FormatException catch (fe) {
  print(fe);
} on Exception catch (e) {
  Print("Some other Exception: " + e);
} catch (u) {
  print("Unknown exception");
} finally {
  print("All done!");
}

A couple of things are noteworthy there. First, you wrap code that might throw an exception (that you want to handle – remember, being unchecked exceptions, you never need to handle any exceptions) in a try block. Then, you catch one or more exceptions as you see fit. Here, the somethingThatMightThrowAnException() function can throw a FormatException, and we want to handle that explicitly. Then, we’ll handle any other object thrown that is a subclass of Exception and display its message. Finally, anything else thrown will be handled as an unknown exception.

Next, notice the syntactic differences there: you can write on <exception_type> catch, or you can just write catch(<object_identifier) where the object identifier is the object that was thrown under whatever name you’d like in the catch block. You can also just write on if you wish. The difference is what you need to do: if you just want to handle the exception but don’t care about the thrown object, you can just write on. If you don’t care about the type but do want the thrown object, then just use catch. When you care both about the type and also need the thrown object, use on <exception_type> catch(<object_identifier).

You can also add a finally clause to a try...catch block. This code will execute whether any sort of exception was thrown or not. This code will execute after any matching catch clauses have finished their work.

Finally, you can define your own exception classes just by extending Exception or Error. You then use them precisely as you do any Dart-provided exception.

I Have the Power: Generators

Sometimes, you have some code that produces some values. Maybe that code relies on some remote system that it needs to call. In that case, you may not want to block the rest of your code from executing while those values are generated. You instead want to generate that list in a “lazy” fashion. Alternatively, you may simply not want or be able to produce the list of values all in one shot. In these situations, a generator is something you’ll want to be familiar with

Dart has two types of generators: synchronous, which returns an Iterable object, and asynchronous, which returns a Stream object. Let’s discuss the synchronous type first by way of example:
Iterable<int> countTo(int max) sync* {
  int i = 0;
  while (i < max) yield i++;
}
main() {
  Iterable it = countTo(5);
  Iterator i = it.iterator;
  while (i.moveNext()) {
    print(i.current);
  }
}

The first thing to note is the sync* marker before the body of the function. This clues Dart into the fact that this is a generator function (and a generator is always a function by the way). The second key point is the use of the yield keyword within the generator. This effectively adds the value to the Iterable that is constructed behind the scenes and returned from the function.

When called, countTo() immediately returns an iterable. Your code can then extract an iterator from that to begin iterating the result list (even though it’s not populated yet). Where it gets interesting is that countTo() won’t actually execute until the code calling it extracts that iterator and then calls moveNext() on it. When that happens, countTo() will execute until the point it hits the yield statement. The expression i++ is evaluated and is “yielded” back to the caller via the “invisible” iterator. Then, countTo() suspends (since it hasn’t met its condition for ending yet), and moveNext() returns true to its caller. Since the code using countTo() is iterating the iterator, we can read the value just yielded via its current property.

Then, countTo() resumes execution the next time moveNext() is called. When the loop ends, the method implicitly executes a return, which causes it to terminate. At that point, moveNext() returns false to its caller, and the while loop ends.

The second type of generator can be demonstrated with this code:
Stream<int> countTo(int max) async* {
  int i = 0;
  while (i < max) yield i++;
}
main() async {
  Stream s = countTo(5);
  await for (int i in s) { print(i); }
}

The difference here are the use of Stream as a return type and the use of the async* marker instead of *sync before the function body. Another difference is in how we use the countTo() method. Since it’s an async method, we need the function it’s called in also to be marked with async. Then, the await for is added. This is a form of for loop that is stream-aware. In a sense, because the for loop is awaiting the countTo() function to do its work, that function is effectively “pushing” the value to the for loop via the returned Stream. In this example, it may not seem obvious why you would do this, but imagine if instead of just incrementing i, countTo() instead was calling a remote server to get the next value. Hopefully, then it becomes more obvious what the value of generators is.

Metatude: Metadata

Dart also supports the notion of metadata embedded in your code. This is usually called annotations in other languages, and it kinda/sorta is in Dart too (I say it that way because the documentation calls this “metadata annotations,” which is a bit verbose, but I suppose it’s more accurate).

Dart provides two annotations, one of which you saw earlier: @override. As previously described, this is used to indicate that a class is intentionally overriding a member of its superclass.

The other annotation Dart provides is @deprecated. No, the annotation isn’t deprecated, what it marks is, silly! This marks something to indicate that you probably shouldn’t be using it anymore and that it might be removed at some point. This is especially common with a class method that will be removed in a future version of your code, but you want to give people using it a bit of time to make the change.

You can also create your own annotations. An annotation is just a class, so this could be an annotation:
class MyAnnotation {
  final String note;
  const MyAnnotation(this.note);
}
Here, the annotation can take an argument, so we can use it like so:
@MyAnnotation("This is my function")
Void myFunction() {
  // Do something
}

You can annotate the following language elements: library, class, typedef, type parameter, constructor, factory, function, field, parameter, variable declaration, and import and export directives. The metadata carried by annotations can be retrieved at runtime using Dart’s reflection capabilities, but I leave discovering that as an exercise for the reader if and when needed as its generally not needed by most people’s application code.

Speaking in General: Generics

Generics are used to inform Dart about the type of something. For example, if you write
var ls = List<String>();

...then Dart knows that the list ls can only contain Strings. It will enforce that type safety at compile-time.

But that almost seems like they should be called specifics, right? You’re telling Dart specifically what type that List holds. Where the name generics comes into play is when you write something like this:
abstract class Things<V> {
  T getByName(String name);
  void setByName(String name, V value);
}

Here, we’re telling Dart that the Things class can be used for any type, where V is a stand-in for the type (by convention, generic types like this are a single letter, most usually E, K, S, T, or V). Now, with this class serving as an interface, you can go off and implement many different versions of it, all using a different type (maybe Person, Car, Dog, and Planet, all of which could implement this same base interface).

Lists and Maps can be defined generically as shown earlier, and you can use the literal form as well:
var brands = <String>[ "Ford", "Pepsi", "Disney" ];
var movieStars = <String, String>{
  "Pitch Black : "Vin Diesel",
  "Captain American" : "Chris Evans",
  "Star Trek" : "William Shatner"
};
In Dart, generic types are reified, which means that their type is carried with them at runtime, allowing you to test the type of a collection with the is keyword:
var veggies = List<String>();
veggies.addAll([ "Peas", "Carrots", "Cauliflower"]);
print(veggies is List<String>);

That will print true, as you’d expect, thanks to reification. While this seems kind of obvious, it’s not the case in every language. Java, for example, uses erasure rather than reification, which means that the type is removed at runtime. So, while you can test that something is a List, you can’t test that it’s a List<String> in Java like you can in Dart.

Finally, methods can use generics as well as class definitions:
class C {
  E showFirst<E>(List<E> lst) {
    E item = lst[0];
    if (item is num) {
      print("It's a number");
    }
    print(item);
    return item;
  }
}
main() async {
  C c = new C();
  c.showFirst(<String>[ "Java", "Dart" ]);
  c.showFirst(<num>[ 42, 66 ]);
}

As you can see, we can feed any type to the showFirst() method, and it can identify the type using the is keyword and act accordingly. That’s one of the key benefits of generics: you don’t need to write two different versions of showFirst(), one to handle strings and one to handle numbers. Instead, a single method can do it just fine. This isn’t necessarily the best example since just print()’ing item will work regardless of what it is, but if you wanted to do more when it’s a number, then this would be ideal.

Summary

In this chapter, you got a look at much of what Dart has to offer. You learned about the basics like data types, operators, comments, logic, and flow control. You also learned some, you might say, mid-level stuff, things like classes, generics, and libraries. Finally, you got an introduction into some slightly more advanced topics such as asynchronous functions, generators, and metadata annotations. From all of this, you should now have a solid foundation of Dart knowledge, plenty with which to start diving into some real Flutter code anyway.

In the next chapter, we’ll do a high-level survey of Flutter, focusing primarily on the widgets it offers. We’ll start putting some of that Dart knowledge to good use while building directly on top of it a layer of Flutter knowledge so that by the time Chapter 4 rolls around, you’ll be well prepared to start building some real projects!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.15.235.104