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.
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.
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.
Anything between those two sequences is ignored.
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
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.
...Dart won’t complain that x now points to a numeric value rather than a string.
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
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.
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
You can see double and single quotes here, and you can see both forms of expressions (sometimes referred to as tokens).
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.
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).
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
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.
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.
...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.
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
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
This will only print() (which writes content to the console) the circumference if the object reference by shape is of type Circle.
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
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.
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
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
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).
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.
Why you might do this is something I’ll touch upon on the section on asynchronous code.
Smooth Operators
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.
A note on the = operator: There is also a ??= operator which does the assignment only if the operand is null.
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.
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.
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
Yep, that’s it!
Instance Variables
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.
That will print “Hi”, all without ever creating an instance of MyClass.
Methods
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.
That also demonstrates that setter methods have indeed been created for us, which is why h.firstName = "Luke"; works.
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!
As with the static variable example, this again prints “Hi”, but this time as a result of calling sayHi() without instantiating MyClass first.
Constructors
The “this” Reference
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
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
Hopefully you can see why!
Interfaces
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
In that case, someMethod() has a default implementation and a subclass therefore does not need to provide one if it doesn’t want to.
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!”
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().
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.
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().
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.
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).
Tell Me Is It So: Assertions
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.
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.
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.
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.
Here, only the Account class from lib1 would be imported, and everything except the Account class from lib2 would be imported.
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.
Exceptions are objects, so you need to construct one to throw.
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!
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
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 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 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
...then Dart knows that the list ls can only contain Strings. It will enforce that type safety at compile-time.
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).
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.
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!