Chapter 2. A brief intro to Dart

This chapter covers

  • Dart’s Hello, World!
  • Anatomy of a Dart program
  • Basic Dart syntax such as control flow, loops, and functions
  • Object-oriented programming in Dart
  • Using I/O Dart libraries

This book is about building mobile apps with Flutter. But you can’t write a Flutter app without learning a bit about the Dart programming language first. The good news is that Dart is also quite easy to pick up. If you’re already comfortable enough with Dart that you understand the following code block, you can skip this chapter:

class CompanyContext extends StateObject {
  final bool isLoading;
  String _companyId;

  CompanyContext({
    this.isLoading = false,
  });
  String get companyId => _companyId;
  void set companyId(String id) {
    _companyId = id;
    _initApp();
  }

  factory CompanyContext.loading() => CompanyContext(isLoading: true);

  @override
  String toString() => 'CompanyContext{isLoading: $isLoading, _companyId: 
 $_companyId}';
}
The command line

Many instructions in this book will involve running commands in your machine’s terminal. I’m a big fan of GUIs, and I don’t use the command line much. You don’t need to be a command-line wizard to use this book. Just know that anytime you see a line of code that starts with a $, it’s a command for your terminal. The following => shows the return value (if any). For example, the command which dart in most Unix system terminals returns the file path to your Dart SDK. You can use it to ensure that you have Dart installed:

$ which dart
=> /usr/local/bin/dart

Before proceeding, make sure Dart is installed on your machine. Installation instructions can be found in appendix A.

2.1. Hello, Dart!

Like all good programming books, we’re going to start with a program that prints “Hello, World” (kind of) to your console. In your favorite text editor, create a new file in the hello_world directory called hello_world.dart. Write this code in the file:

void main() {
  print('Hello, Dart!');
}

Now, back in your terminal, you can execute Dart code with the CLI command dart. To follow along with these instructions, make sure you’re in the right project directory where your hello_world.dart file lives. Then, run the “Hello, Dart” example:

$ dart hello_world.dart
// => Hello, Dart!

If “Hello, Dart!” did in fact print, congrats! You wrote your first Dart program, and you’re officially a Dart programmer!

2.1.1. Anatomy of a Dart program

Dart programs of all shapes and sizes have a few things in common that must exist: most important, a main function in the entry file of your program. This is the first piece of the puzzle. Figure 2.1 shows a Dart function definition.

Figure 2.1. The main function in Dart

All Dart functions look like this, but main is special. It’s always the first code that’s executed in a Dart program. Your program must contain a main function that the Dart compiler can find.

Notice the word void in the example. void is the return type of this function. If you haven’t used typed languages, this may look strange. Types are a core part of writing Dart code. Every variable should have a type, and every function should return a type (or void). void is a keyword that means “this function doesn’t return anything.” We’ll dive deeper into the type system when we have more robust examples to walk through. But for now, just remember: all functions return a type (or void).

Next in this example is the line that contains the print function:

print('Hello, Dart!');

print is a special function in Dart that prints text to the console.

On that same line, you have a String ('Hello, Dart!'), and you have a semicolon (;) at the end of the line. This is necessary at the end of every statement in Dart. That’s all you need to write a Dart program.

This section will introduce the basics of setting up and running a Dart program. It will also introduce some common Dart syntax. Finally, we’ll use the dart:io package to learn about importing libraries, and standard-in and standard-out.

2.1.2. Adding more greetings

I want to expand on the greeter example so I can talk about some of Dart’s basic syntax. In general, a lot of Dart syntax is similar to many languages. Control flow, loops, and primitive types are as you’d expect if you come from almost any language.

Let’s write a program that will output this to the console:

Hello, World!
Hello, Mars!
Hello, Oregon!
Hello, Barry!
Hello, David Bowie!

There’s a lot to learn in simple examples. To start, refactor the old example to create a separate function that prints. Your file will look like this:

void main() {
  helloDart();             1
}

void helloDart() {         2
  print('Hello, Dart!');
}

  • 1 To call a function, add () to the end, with no body.
  • 2 A second function declaration

Next, the helloDart function needs to be told what to print, because you don’t want it to print “Hello, Dart!” forever. You’ll do this by passing in a name to replace “World.” You pass in arguments to functions by putting a type and variable name in the () in the function signature:

void helloDart(String name) {      1
  print('Hello, $name');
}

  • 1 This function now expects a name argument. Trying to call this function with anything other than exactly one argument, of the type String, is an error.

You don’t just want to print one name, though; you want to print a sequence of names. The names will come from a hard-coded List, Dart’s basic array-like data structure. A List manages its own size and provides all the functional programming methods you expect for an array, like map and forEach.

For now, you can create a list with the list-literal constructor using square brackets: var myList = [a,b,c]. Add a list of names to your main function:

void main() {
  List<String> greetings = [    1
    'World',
    'Mars',
    'Oregon',
    'Barry',
    'David Bowie',
  ];
  helloDart();
}

  • 1 Defines a collection type, which has objects in it that have a type: in this case, a List filled with Strings. Another example would be Map<int, String>.

Right now, there’s an error in the program. This sample calls helloDart() without a string as an argument. We need to be passing each of those individual greetings in to the call to helloDart(). To do so, loop over the greetings variable and call the helloDart() function inside every iteration of the loop:

void main() {
  List<String> greetings = [
    'World',
    'Mars',
    'Oregon',
    'Barry',
    'David Bowie',
  ];
  for (var name in greetings) {     1
    helloDart(name);                2
  }
}

  • 1 A for..in loop is similar to other languages. It hits each member of the list once, in order, and exposes it as a variable in the code block.
  • 2 Passes the variable exposed by the for..in loop in to the helloDart call

Finally, update the helloDart method to print “Hello” followed by the specific greeting. This is done with interpolation. String interpolation in Dart uses the ${} syntax for expressions, or just a $ for single value. Here is the full example.

Listing 2.1. Dart for-in loops
void main() {
    List<String> greetings = [
    'World',
    'Mars',
    'Oregon',
    'Barry',
    'David Bowie',
  ];
  for (var name in greetings) {
    helloDart(name);
  }
}

void helloDart(String name) {
  print('Hello, $name');
}

That’s all it takes. This should work now.

2.1.3. I/O and Dart libraries

The final feature in this example is interacting with a user. The user will be able to say who they want to greet.

The first step is importing libraries. The Dart SDK includes many libraries, but the only one that’s loaded in your program by default is dart:core. Some common libraries in the Dart SDK are dart:html, dart:async, and dart:math. Different libraries are available in different environments. For example, dart:html isn’t included in the Flutter SDK, because there’s no concept of HTML in a Flutter app. When writing a server-side or command-line application, you’ll probably use dart:io. Let’s start there.[1]

1

For more information on the dart:io library, check out the official docs at http://mng.bz/5AmZ.

To import any library, you only need to add an import statement at the top of a Dart file:

import 'dart:io';

We won’t use standard input and outputs in this book much, if at all, after this, so don’t get bogged down in the details of the io library right now. This program asks for a name from the user on the command line and then greets that person.

Listing 2.2. I/O in Dart
import 'dart:io';

void main() {                            1
  stdout.writeln('Greet somebody');      2
  String input = stdin.readLineSync();   3
  return helloDart(input);
}

void helloDart(String name) {
  print('Hello, $name');
}

  • 1 Again, main is the start of the program.
  • 2 stdout.writeln is functionally the same as print but can also be used to write text to files.
  • 3 readLineSync is a blocking function that stops execution and waits for the user to respond in the command line.

You could improve this program by looping over everything and repeatedly asking for a name, or make it a number guessing game that exits when you guess the write number. I’ll leave that for you to do on your own.

2.2. Common programming concepts in Dart

If all programming languages can be described in terms of the Beatles catalogue, Dart is like the Beatles’ greatest hits. Everyone loves the Beatles, because the Beatles are great. And everyone knows the song “Hey Jude.” But when you’re listening to the Beatles’ greatest hits at a fun, upbeat party, you’re never worried that you’re going to suddenly be listening to the song “Within You Without You.” I love that song, but it’s not for everyone, and it’s certainly not a party song. When writing Dart code, you’re never scared that you’re going to run into unexplainable syntax or behavior: it’s all expected and, in the worst-case scenario, easy to grok when you look at the docs.

There are a few important concepts you should keep in mind while writing Dart code:

  • Dart is an object-oriented language and supports single inheritance.
  • In Dart, everything is an object, and every object is an instance of a class. Every object inherits from the Object class. Even numbers are objects, not primitives.
  • Dart is typed. You cannot return a number from a function that declares it returns a string.
  • Dart supports top-level functions and variables, often referred to as library members.
  • Dart is lexically scoped.

And Dart is quite opinionated. In Dart, as in all programming languages, there are different ways of getting things done. But some ways are right and some are wrong. This quote from the Dart website sums it up: “Guidelines describe practices that should always be followed. There will almost never be a valid reason to stray from them.”

Just in time: Typed programming languages

A language is typed if every variable’s type is known (or inferred) at compile-time. In human English, a language is typed if you, as the developer, can (or must) explicitly assign types to variables in your code. A language is dynamic if the types are inferred at runtime. JavaScript, Python, and Ruby are dynamic languages. (Under the hood, though, all languages are typed to some degree.)

Types are used because they make your code safer. Your compiler won’t let you pass a string to a function that expects a number. Importantly, in Dart, this type check is done at compile time. This means you’ll never ship code that crashes because a function doesn’t know what to do with a different type of data than it expects.

The biggest benefit of using a type system is that it reduces bugs.

2.2.1. Intro to Dart’s type system

The type system in Dart is something I’ll discuss throughout the book. The type system is straightforward (as far as type systems go). That said, it has to be briefly examined before I can talk about anything else. It’s more complicated than many subjects, such as if statements, but it must be learned first. I encourage you to circle back to this section at any time throughout the book if you need a type-system refresher.

Before I became a Dart developer, I wrote Ruby, Python, and JavaScript, which are dynamic. They have no concept of types (to the developer). When I started writing Dart, I found using types to be the biggest hurdle. (But now, I don’t want to live in a world without them.)

There are a few key places that you need to know about types for now, and the rest will be covered in time. First, when declaring variables, you give them a type:

String name;     1
int age;

  • 1 The type always comes before the value it describes.

Using types prevents you from assigning values to variables that aren’t compatible:

int greeting = 'hello';

If you try to compile that file by running it in your terminal, you’ll get this error:

Error: A value of type 'dart.core::String' can't
    be assigned to a variable of type 'dart.core::int'.
Try changing the type of the left hand side,
    or casting the right hand side to 'dart.core::int'.

First, that’s a pretty darn good error message, as error messages tend to be in Dart. (Thanks, Dart team.) But also, this is what’s called type safe. Types ensure at compile time that your functions all get the right kind of data. This reduces the number of bugs you get at runtime.

Tip

If you’re using one of the IDEs suggested in the appendix and have installed the Dart plugin, you won’t even get that far. The linter will tell you you’re using the wrong type straight away. This is, in a nutshell, the value of type systems.

Complex data types

When using data structures like a List or Map, you use < and > to define the types of values within the List:

List<String> names;          1
List<int> ages;              2
Map<String, int> people;     3

  • 1 A list of strings
  • 2 A list of integers
  • 3 A map whose keys are strings and values are integers
Types in functions

Recall from earlier that the main function has a return type of void. Any function that’s being used for its side effects should have this return type. It means the function returns nothing.

Other functions, though, should declare what they’re going to return. This function returns an int:

int addNums() {
  // return an int
}

The second place you use types in functions is when you define arguments:

int addNums(int x, int y) {
  return x + y;
}
Dynamic types

Dart supports dynamic types as well. When you set a variable as dynamic, you’re telling the compiler to accept any type for that variable:

dynamic myNumber = 'Hello';

Technically, you could just mark everything as dynamic, which makes Dart optionally typed. And to that, I’d say “Good luck!” This would remove the benefits of using types, but still force you to write the word dynamic everywhere. I point that out because there are some instances in which you don’t explicitly assign a type:

var myString = 'Hello';

This works, but if you then tried to set myString to 3, the compiler would throw an error. Once a variable is given a type, that’s its type forever. Also, functions don’t have to be annotated with a return type:

myPrint() {           1
    print('hello');
}

  • 1 Notice the lack of return type.

This works, but the type is still inferred. Trying to assign the return value of myPrint to a variable would throw an error:

// doesn't work
var printer = myPrint();

This doesn’t work, because there is no return value.

Should you ever use dynamic types?

dynamic comes in handy. It’s pretty common to use dynamic in maps. Perhaps you’re working with JSON:

Map<String, dynamic> json;

If you’re converting some JSON to a Dart object, you know the keys of the Map are going to be strings, but the values could be strings, numbers, lists, or another map.

As for the var keyword, its usefulness is a matter of code style. The var keyword can only be used to define variables and cannot be used to define a type, unlike dynamic. In other words, this isn’t valid:

Map<String, var> json;     1

  • 1 Invalid use of var!

So, the scope of where var can be used is small. In the cases where it’s valid, you should prefer to use the actual type of the variable you’re defining (if the variable is reassignable). If the variable shouldn’t be reassignable, it’s common to use final, without any type definition. This is almost always done in the bodies of functions, and not as class members. Otherwise, you’re better off using types.

2.2.2. Comments

Dart supports three kinds of comments:

// Inline comments

/*
Blocks of comments. It's not convention to use block comments in Dart.
*/

/// Documentation
///
/// This is what you should use to document your classes.

Generally, documentation comments with three slashes are used for documenting your code. Anything you think will stay in the code forever should use this style of comments. Inline comments are used for brief insights into something on a specific line.

2.2.3. Variables and assignment

Variables are used in Dart to tell an object or class to hold onto some local state. Establishing a variable in Dart is as you’d expect. This is a variable definition:

String name;

That line simply tells your program that there will be a value called name, but the value is yet to be determined (but, in this case, it will be a String). At this point, name hasn’t been assigned to a value, so its value is null. All unassigned variables in Dart are null. null is a special value that means “nothing.” In Dart, null is an object, like everything else. That’s why ints, Strings, Lists, and everything else can be assigned to null. Technically, you could do this:

int three = null;

According to the Dart style guide, you should avoid explicitly assigning objects to null (see http://mng.bz/om52).

final, const, and static

These three keywords “extend” the type of the variable. The first two, final and const, are similar. You should use these keywords if you want to make a variable immutable (in other words, if you never intended to change the value of the variable). The difference in the two is subtle.

final variables can only be assigned once. However, they can be declared before they’re set at the class level. Or, in English, a final variable is almost always a variable of a class that will be assigned in the constructor. If those terms aren’t familiar, don’t worry. They’ll be covered in depth later.

const variables, on the other hand, won’t be declared before they’re assigned. Constants are variables that are always the same, no matter what, starting at compile time.

This is acceptable:

const String name = 'Nora';

But this is not acceptable:

const String name = 'Nora $lastName';

That value in the second example could change after compile time. For example, it could be “Nora Smith” or “Nora Williams.” Therefore the variable name is not allowed to be marked const.

const variables should be used whenever possible, as they boost performance. In Flutter, there are special tools to help make your classes and widgets const. I’ll cover that later.

Lastly, there is a modifier called static. static methods are used solely in classes, so I’ll discuss them later as well.

2.2.4. Operators

There aren’t any big surprises in Dart operators, as you can see in table 2.1.

Table 2.1. Dart operators

Description

Operators

Arithmetic * / % ~/ + -
Relational and type test >= > <= < as is is!
Equality == !=
Logical and/or && ||
Assignment = *= /= ~/= %= += -= <<= >>= &= ^= |= ??=
Unary expr++ expr-- . ?. -expr !expr ~expr ++expr --expr

I’d like to point out a couple of these operators that are used often but may not be familiar to you:

  • ~/ is the symbol for integer division. This never returns a decimal point number, but rather rounds the result of your division to the nearest integer. 5 ~/ 2 == 2
  • as is a keyword that typecasts. This has everything to do with classes and object orientation, so I’ll cover it later.
  • is and is! check that two objects are the same type. They are equivalent to == and !=.
  • In the unary row, ignore the word “expr.” That’s only used to make the operators readable.

2.2.5. Null-aware operators

Null-aware operators are one of my favorite features in Dart. In any language, having variables and values fly around that are null can be problematic. It can crash your program. Programmers often have to write if (response == null) return at the top of a function to make asynchronous calls. That’s not the worst thing ever, but it’s not concise. I use Go quite a bit, and it isn’t a robust language. (That’s not a judgment, it’s a statement of fact.) About once in every 10 lines of code, there’s an if statement checking for nil. This makes for some robust functions.

Null-aware operators in Dart help resolve this issue. They’re basically ways to say, “If this object or value is null, then forget about it: just cut out here, but don’t throw an error.”

The number one rule of writing Dart code is to be concise but not pithy. Anytime you can write less code without sacrificing readability, you probably should. The three null-aware operators that Dart provides are ?., ??, and ??=, and I’ll explain them next.

The ?. operator

Suppose you want to call an API and get some information about a User. And maybe you’re not sure whether the user information you want to fetch even exists. You can do a standard null check like this:

void getUserAge(String username) async {
  final request = new UserRequest(username);    1
  final response = await request.get();
  User user = new User.fromResponse(response);
  if (user != null) {                           2
    this.userAge = user.age;
  }
  // etc.
}

  • 1 await is syntactic sugar for writing async code. We’ll go over it in depth later.
  • 2 A standard null check without null-aware operators

That’s fine. It works. But the null-aware operators make it much easier. The following operator basically says, “Hey, assign userAge to user.age. But if the user object is null, that’s okay. Just assign userAge to null, rather than throwing an error”:

void getUserAge(String username) async {
  final request = UserRequest(username);
  final response = await request.get();
  User user = new User.fromResponse(response);
  this.userAge = user?.age;                      1
  // etc.
}

  • 1 Delightfully shorter null check

If user is indeed null, then your program will assign userAge to null, but it won’t throw an error, and everything will be fine. If you removed the ?. operator, it would be an error to call age on a null User object. Plus, your code is more concise and still readable. That’s the key: clean, concise code.

Note

It’s worth pointing out that if any code below the line this.userAge = user?.age; relied on useAge not being null, the result would be an error.

The ?? operator

The second null-aware operator is perhaps even more useful. Suppose you want the same User information, but many fields for the user aren’t required in your database. There’s no guarantee that there will be an age for that user. Then you can use the double question mark (??) to assign a “fallback” or default value.

This operator says, “Hey program, do this operation with this value or variable. But if that value or variable is null, then use this backup value.” It allows you to assign a default value at any given point in your process, and it’s super handy:

void getUserAge(String username) async {
  final request = new UserRequest(username);
  final response = request.get();
  Useruser = new User.fromResponse(response);
  this.userAge = user.age ?? 18;               1
  // etc.
}

  • 1 If user.age is null, defaults to 18
The ??= operator

This last null-safe operator accomplishes a goal pretty similar to the previous one, but the opposite. While writing this, I was thinking about how I never use this operator in real life. So I decided to do a little research. And wouldn’t you know it? I should be using it. It’s great.

This operator basically says, “Hey, if this object is null, then assign it to this value. If it’s not, just return the object as is”:

int x = 5
x ??= 3;

In the second line, x will not be assigned 3, because it already has a value. But like the other null-aware operators, this one seeks to make your code more concise.[2]

2

To learn more, read Seth Ladd’s blog post “Null-Aware Operators in Dart” at http://mng.bz/nvee.

2.3. Control flow

The strangest thing about technology is that we treat computers like they’re smart, but they’re actually really dumb. They only know how to do roughly two or three things. You can expect a human, or even a dog, to react appropriately to any given number of situations. Dogs know that if they’re hungry, they need to eat to survive; and they know what’s food and what isn’t. They aren’t going to accidentally eat a rock and hope it works out.

Computers aren’t as nice to work with. You have to tell them everything. They’re quite needy, actually. So we have to take great pains to ensure that no matter what situation arises, the computer knows how to handle it. This is basically why we have control flow, which is the basis for pretty much all logic.

Control flow in Dart is similar to most of the high-level languages. You get if statements, ternary operations, and switch statements.

2.3.1. if and else

Dart supports if, else if, and else, as you’d expect. Here’s a standard if statement:

if (inPortland) {
  print('Bring an umbrella!');
} else {
  print('Check the weather first!');
}

Inside your conditions, you can use && for “and” and || for “or”:

if (inPortland && isSummer) {
  print('The weather is amazing!');
} else if(inPortland && isAnyOtherSeason) {
  print('Torrential downpour.');
} else {
  print ('Check the weather!');
}

Finally, Dart is sane, and a condition must evaluate to a Boolean. There is only one way to say “true” (true) and one way to say “false” (false). In some languages, there is a concept of “truthiness,” and all values coerce to true or false. In such languages, you can write if (3) {, and it works. That is not the case in Dart.

2.3.2. switch and case

switch statements are great when there are many possible conditions for a single value. These statements compare ints, Strings, and compile-time constants using ==. In other words, you must compare a value to a value of the same type that cannot change at runtime. If that sounds like jargon, here’s a simple example:

int number = 1;
switch(number) {
  case 0:
    print('zero!');
    break;             1
  case 1:
    print('one!');
    break;
  case 2:
    print('two!');
    break;
  default:
    print('choose a different number!');
}

  • 1 The switch statement must be told to exit, or it will execute every case. Cases should always end in a break or return statement. More on this in a bit.

That’s perfectly valid. The variable number could have any number of values: it could be 1, 2, 3, 4, 66, 975, -12, or 55. As long as it’s an int, its a possible value for number. This switch statement is simply a more concise way of writing an if/else statement. Here’s an overly complex if/else block, for which you should prefer a switch statement:

int number = 1;
if (number == 0) {
    print('zero!');
} else if (number == 1) {
    print('one!');
} else if (number == 2) {
    print('two!');
} else {
    print('choose a different number!');
}

That’s what a switch statement does, in a nutshell. It provides a concise way to check for any number of values. It’s important, though, to remember that it only works with runtime constants. This is not valid:

intfive = 5;
  switch(five) {
      case(five < 10):     1
      // do things...
  }

  • 1 five < 10 isn’t definitely constant at compile time and therefore cannot be used. It could be true or false. You cannot do computation within the case line of a switch statement.

2.3.3. Advanced switch usage

In switch statements, you can fall through multiple cases by not adding a break or return statement at the end of a case:

intnumber = 1;
switch(number) {
  case -1:
  case -2:
  case -3:
  case -4:
  case -5:
    print('negative!');
    break;
  case 1:
  case 2:
  case 3:
  case 4:
  case 5:
    print('positive!');
    break;
  case 0:
  default:
    print('zero!');
    break;
}

In this example, if the number is between -5 and -1, the code will print negative!.

Exiting switch statements

Each case in a switch statement should end with a keyword that exits the switch. If it doesn’t, you’ll get an error:

switch(number) {
  case 1:
    print(number);
    // ERROR!
  case 2:
  //...

Most commonly, you’ll use break or return. break simply exits out of the switch; it doesn’t have any other effect. It doesn’t return a value. In Dart, a return statement immediately ends the function’s execution, and therefore it will break out of a switch statement.

In addition to those, you can use the throw keyword, which throws an error. (More on throw in a bit; it will always exit a function as well.) Finally, you can use a continue statement and a label if you want to fall through but still have logic in every case:

Stringanimal = 'tiger';
switch(animal) {
  case 'tiger':
    print('it's a tiger');
    continue alsoCat;
  case 'lion':
    print('it's a lion');
    continue alsoCat;
  alsoCat:
  case 'cat':
    print('it's a cat');
    break;
  // ...
}

This switch statement will print it's a tiger and it's a cat to the console.

Ternary operator

The ternary operator is technically that: an operator. But it’s also kind of an if/else substitute. And it’s also kind of a ??= alternative, depending on the situation. I use ternaries in Flutter widgets quite a bit. The ternary expression is used to conditionally assign a value. It’s called ternary because it has three portions—the condition, the value if the condition is true, and the value if the condition is false:

This code says, “If this user’s title is ‘Boss,’ change her name to uppercase letters. Otherwise, keep it as it is.”

2.3.4. Loops

You can repeat expressions in loops using the same keywords as in many languages. There are several kinds of loops in Dart:

  • Standard for
  • for-in
  • forEach
  • while
  • do while

Each of these works exactly as it does in every programming language I’ve come across. So, I’ll just provide some quick examples.

for loops

If you need to know the index, your best bet is the standard for loop:

for (var i = 0; i < 5; i++) {
  print(i);
}

If you don’t care about the index, the for-in loop is a great option:

List<String> pets = ['Odyn', 'Buck', 'Yeti'];
for (var pet in pets) {
  print(pet);
}

An alternative, and probably the preferred way to loop if you don’t care about the index, is using the method on iterables called forEach:

List<String>pets = ['Abe', 'Buck', 'Yeti'];
pets.forEach((pet) => pet.bark());

forEach is special in two ways. First, it’s a function that you call on a List. The practical implication is that it creates a new scope. Any value you have access to in the forEach loop is not accessible thereafter.

Second, the logic in forEach blocks can only provide side effects. That is, you cannot return values. These loops are generally useful for mutating objects, but not for creating new ones.

Note

forEach is a higher-order function. That topic will be explored shortly.

while loops

Again, while loops behave exactly as you’d expect. They evaluate the condition before the loop runs—meaning it may never run at all:

while(someConditionIsTrue) {
  // do some things
}

do-while loops, on the other hand, evaluate the condition after the loop runs. So they always execute the code in the block at least once:

do {
  // do somethings at least once
} while(someConditionIsTrue);
break and continue

These two keywords help you manipulate the flow of the loop. Use continue in a loop to immediately jump to the next iteration, and use break to break out of the loop completely:

for (var i = 0; i < 55; i++) {
  if (i == 5) {
    continue;
  }
  if (i == 10) {
    break;
  }
  print(i);
}

This loop will print the following:

0
1
2
3
4
6
7
8
9

2.4. Functions

Functions look familiar in Dart if you’re coming from any C-like language. We’ve already seen a couple examples of this, via the main function. Now we’ll dig deeper into functions and see how they’re written in Dart. Here’s a basic function:

void main() {

}

2.4.1. Anatomy of a Dart function

The anatomy of a function is pretty straightforward:

String makeGreeting(String name) {      1
  return 'Hello, $name';                2
}

  • 1 Function signature
  • 2 Return type

The function signature follows this pattern: ReturnType functionName(ArgumentType arg). And every function that uses return must have a return type—otherwise, its return type is void.

It’s important to note that Dart is a true object-oriented language. Even functions are objects, with the type Function. You can pass functions around and assign them to variables. Languages that support passing functions as arguments and returning functions from functions usually refer to these as higher-order functions. We’ll explore higher-order functions in depth when we start writing Flutter apps.

Dart also supports a nice shorthand syntax for any function that has only one expression. In other words, is the code inside the function block only one line? Then it’s probably one expression, and you can use this syntax to be concise:

String makeGreeting(String name) => 'Hello, $name';

In this book, we’ll call this an arrow function. Arrow functions implicitly return the result of the expression. => expression; is essentially the same as { return expression; }. There’s no need to (and you can’t) include the return keyword.

2.4.2. Parameters

Dart functions allow positional parameters, named parameters, and optional positional and named parameters, or a combination of all of them. Positional parameters are simply what we’ve seen so far:

void debugger(String message, int lineNum) {
  // ...
}

To call that function, you must pass in a String and an int, in that order:

debugger('A bug!', 55);
Named parameters

Dart supports named parameters. Named means that when you call a function, you attach the argument to a label. This example calls a function with two named parameters:

debugger(message: 'A bug!', lineNum: 44);

Named parameters are written a bit differently. You wrap any named parameters in curly braces ({ }). This line defines a function with named parameters:

void debugger({String message, int lineNum}) {

Named parameters, by default, are optional. But you can annotate them and make them required:

Widget build({@required Widget child}) {
  //...
}

In order to annotate variables with the required keyword, you must use a Dart library called meta. More on this when we get into the Flutter work.

The pattern you see here will become familiar when we start writing Flutter apps. For now, don’t worry too much about annotations.

Positional optional parameters

Finally, you can pass positional parameters that are optional, using [ ]:

int addSomeNums(int x, int y, [int z]) {
  int sum = x + y;
  if (z != null) {
    sum += z;
  }
  return sum;
}

You call that function like this:

addSomeNums(5, 4)        1
addSomeNums(5, 4, 3)     2

  • 1 The third parameter is optional, so you don’t have to pass in anything.
  • 2 You can pass in a third argument, since you’ve defined an optional parameter.

2.4.3. Default parameter values

You can define default values for parameters with the = operator in the function signature:

addSomeNums(int x, int y, [int z = 5]) => x + y + z;

2.4.4. Advanced function concepts

Functions are the bread and butter of reusable code because they let us define our own vocabulary in our programs. In a robust app, there are likely thousands of lines of code. It’s easy to get lost. When used correctly, higher-order functions help add a layer of abstraction to our code that makes it easy to reason about. Consider these two examples that do math:

List<int> nums = [1,2,3,4,5];

int i = 0;
int sum = 0;
while (i < nums.length) {
  sum += nums[i];
  i+=1;
}
print(sum);

List<int> nums = [1,2,3,4,5];

print(addNumbers(nums));

It’s possible that the addNumbers function (not shown here) is implemented exactly the way the first example adds the numbers. But the second example adds a nice layer of abstraction and tells you exactly what it’s doing, as if you’re reading English. You don’t have to read each line of code to understand what’s happening. And as a bonus, you know that if the addNumbers function is bug-free, it will remain bug-free every time you use it in an app. This is a simple example, of course, but breaking up functions into single-responsibility chunks of logic makes them much easier to get right.

Creating your own vocabulary by breaking up functions is called abstraction. Remember, computers are dumb. We have to tell them exactly what we want them to do. But humans are smart. We use abstraction to write low-level, explicit instructions for the computer, and then we wrap it up in nice little functions for future programmers who will be reading our code.

In Dart, abstracting away logic is possible because it supports these higher-order functions. A function is higher-order if it accepts a function as an argument or if it returns a function. In other words, higher-order functions operate on other functions. If you aren’t sure about higher-order functions, you’ve likely seen them before in a different language:

List<int> nums = [1,2,3];
nums.forEach((number) => print(number + 1));

forEach is a higher-order function because it takes a function as its argument. Another way to write that would be like this:

void addOneAndPrint(int num) {
  print(num +1);
}

nums.forEach(addOneAndPrint);
Note

The first forEach example uses an anonymous function, which means it doesn’t have a name. It’s defined right there in the argument to forEach, and after it’s executed, it’s gone forever.

Earlier, I mentioned that functions are just objects, like everything in Dart. That’s why you can use functions like you can any other object, including passing them around as variables and return values.

In reality, you can get away without writing your own logic that uses higher-order functions. But you’ll likely come across them in the wild when processing Iterable objects. (List, for example, is an Iterable, because you can iterate over it.) Iterable objects (and Map objects, to an extent) provide functions like forEach, map, and where, which are higher-order functions that perform some task on every member in the list. I’ve already discussed forEach, but let’s look at an example using map.

List.map is the same as forEach in that it takes a function as an argument, and that function is called with each member of the list as an argument. It’s different from forEach in that it returns a value from each function call, and the return values are added to a new list. For example:

List<int> smallNums = [1,2,3];
Iterable<int> biggerNums = smallNums.map((int n) => n * 2);      1

  • 1 List.map takes a function as its argument. Each time the inner function is called, it’s passed a member of the smallNums list as an argument.

This code looks at each member of smallNums and calls a function on it. In this case, that function is (int n) ? num * 2. So it’s going to call the function once for 1, once for 2, and once for 3. The list biggerNums is [2, 4, 6]. Even though you can get away with not using too many higher-order functions, you’ll see how useful they can be in Flutter development.

2.4.5. Lexical scope

Dart is lexically scoped. Every code block has access to variables “above” it. The scope is defined by the structure of the code, and you can see what variables are in the current scope by following the curly braces outward to the top level:

String topLevel = 'Hello';

void firstFunction() {
  String secondLevel = 'Hi';
  print(topLevel);
  nestedFunction() {
    String thirdLevel = 'Howdy';
    print(topLevel);
    print(secondLevel);
    innerNestedFunction() {
      print(topLevel);
      print(secondLevel);
      print(thirdLevel);
    }
  }
  print(thirdLeve);
}

void main() => firstFunction();

This is a valid function, until the last print statement. The third-level variable is defined outside the scope of the nested function, because scope is limited to its own block or the blocks above it. (Again, a block is defined by curly braces.)

2.5. Object-oriented programming (in Dart)

Modern applications basically all do the same thing: they give us (smart humans) a way to process and collaborate large data sets. Some apps are about communication, like social media and email. Some are about organization, such as calendars and note taking. Some are simply digital interfaces into a part of the real world that’s hard for programmers to navigate, like dating apps. But they all do the same thing. They give users a nice way to interact with data.

Data represents the real world. All data describes something real. That’s what object-oriented programming is all about: it gives us a nice way to model our data after real-world objects. It takes data, which dumb computers like, and adds some abstraction so smart humans can impose our will onto the computers. It makes code easy to read, easy to reason about, and highly reusable.

When writing Dart code, you’ll likely want to create separate classes for everything that can represent a real-world “thing.” Thing is a carefully chosen word, because it’s so vague. (This is a great example of something that would make a dumb computer explode but that a smart human can make some sense of.)

Consider if we were writing a point-of-sale (POS) system used to sell goods to customers. What kinds of classes do you think you’d need to represent “things” (or data)? What kind of “things” does a POS app need to know about? Perhaps we need classes to represent a Customer, Business, Employee, Product, and Money. Those are all classes that represent real-world things. But it gets a bit hairier from here.

Ponder some questions with me:

  • We may want a class for Transaction and Sale. In real life, a transaction is a process or event. Should this be represented with a function or a class?
  • If we’re selling bananas, should we use a Product class and give it a property that describes what type of product it is? Or should we have a Banana class?
  • Should we define a top-level variable or a class that has only a single property? For instance, if we need to write a function that simply adds two numbers together, should we define a Math class with an add method, or just write the method as a static, global variable?

Ultimately, these decisions are up to you, the programmer. There is no single right answer.

2.5.1. Classes

My rule of thumb is, “When in doubt, make a new class.” Recall those previous questions: Should a transaction be represented by a function of Business or its own class? I’d say make it a class. And that brings me all the way back to why I used the vague word thing earlier. A thing isn’t just a physical object; it can be an idea, an event, a logical grouping of adjectives, and so on. In this example, I would make a class that looks like this:

class TransactionEvent {       1
  // properties and methods
}

  • 1 Uses the class keyword to define a new class

And that might be it. It might have no properties and no methods. Creating classes for events makes the type safety of Dart that much more effective.

The bottom line is that you can (and, I’d argue, should) make a class that represents any “thing” that isn’t obviously an action you can do or a word you’d use to describe some detail of a “thing.” For instance, you (a human) can exchange money with someone. It makes sense to say, “I exchange money.” It doesn’t make sense to say, “I transaction,” even though a transaction is an idea. Having a Transaction class makes sense, but an ExchangeMoney class doesn’t.

Nearly all the code you write in Dart will be contained in classes. And a class is a blueprint for an object. That is, a class describes an object that you can create. The object itself is what holds any specific data and logic. For example, a Cat class might look like this:

class Cat {
    String name;
    String color;
}

This class describes an object that can be created, like this:

Cat nora = new Cat();
nora.name = 'Nora';
nora.color = 'Orange';

A note about the (lack) of the new keyword

If you’re coming from many other object-oriented languages, you’ve probably seen the new keyword used to create new instances of a class. In Dart, this new keyword works the same way, but it isn’t necessary. In Dart 2, you don’t need to use new or const to create an object. The compiler will infer that for you. More on the motivation behind this language feature in chapter 3.

From here on out, I will not use the new keyword, as it’s considered bad practice in Dart.

The Cat class itself doesn’t have any information. It’s a blueprint. The nora object, though, is a Cat instance. It has a name and color, and those aren’t related to any new instances of Cat that are made in the future. You could, later in the code, create a new cat:

Cat ruby = Cat();
nora.name = 'Ruby';
nora.color = 'Grey';

nora and ruby are completely separate. They are instances of the class. After writing the class, you generally don’t interact with the class itself, but rather the instances of (aka objects created by) the class.

Note

There are a couple of caveats when you want to interact with the class directly, which I’ll cover as we go.

2.5.2. Constructors

You can give classes special instructions about what to do as soon as a new instance is created. These functions are called constructors.

Often, when creating a class, you’ll want to pass values to it or perform some initialization logic. You can use the constructor to assign those values to properties of an instance of that class:

class Animal {
  String name;                         1
  String type;

  Animal(String name, String type) {   2
    this.name = name;                  3
    this.type = type;
  }
}

  • 1 Declares properties of this class (they are null to start)
  • 2 Default constructor
  • 3 Passes in arguments to the constructor

A default constructor is written as a function that shares a name with the class. Any arguments that need to be passed in to the function, to be assigned to the properties of the class, are defined just like function arguments. You can pass in arguments to the constructor and assign them to the instance properties of the same name.

In some languages, you have to explicitly assign each property to the variable you passed to the constructor, like the previous example (for example, calling this.name = name in the constructor body). Dart provides some nice syntactic sugar to make the code less verbose. You can achieve the same thing like this:

class Animal {
  String name, type;

  Animal(this.name, this.type);    1
}

  • 1 Automatically assigns arguments to properties with the same name

You can put whatever code and logic you want in a constructor. It’s just a plain ol’ function:

class Animal {
  String name, type;

  Animal(this.name, this.type) {
    print('Hello from Animal!');
  }
}

Earlier, I referred to a constructor as a default constructor. There are other types of constructors, and classes can have multiple constructors. I will cover those later in this book. But first, I want to talk about the next important topic in object-oriented programming: inheritance.

2.5.3. Inheritance

In object-oriented programming, inheritance is the idea that a class can inherit or subclass a different class. A cat is a specific kind of mammal, so it follows that a cat will have all the same functionality and properties as all other mammals. You can write a Mammal class once, and then both the Dog and Cat classes can extend the Mammal class. Both of those classes will then have all the functionality of the Mammal class:

class Cat extends Mammal {}   1
class Eric extends Human {}
class Honda extends Car {

  • 1 Uses extends to inherit all of a superclass’s functionality

When a class inherits from another class (called its superclass), it’s essentially a copy of the superclass, and you can add extra functionality—whatever you define in the class itself. (For example, Cat is a copy of the Mammal class, but you can also add a function called meow.) Let’s look at a small, concrete example:

// superclass
class Animal {
  String name;
  int legCount;
}

// subclass
class Cat extends Animal {
  String makeNoise() {
    print('purrrrrrr');
  }
}

In this example, if we made an instance of Cat, then it would have properties called name and legCount:

Catcat =  Cat();
cat.name = 'Nora';
cat.legCount = 4;
cat.makeNoise();

Those are all perfectly valid expressions. You can set the cat’s name, because it’s also an Animal. This is not valid, however:

Catcat =  Animal();
cat.makeNoise();

Animal is the superclass and has no concept of or relationship to any of the subclasses that extend it.

To expand on inheritance, consider if we made a class that’s almost exactly the same, but for a Pig:

class Pig extends Animal {
  String makeNoise() {
    print('oink');
  }
}

It’s now perfectly valid to do this:

Pig pig =  Pig();
pig.name = 'Babe';
pig.legCount = 4;
pig.makeNoise();

Since Pig extends Animal, like Cat, it has a name property and a legCount property. Finally, inheritance is like a tree. If Pig inherits from Mammal, which inherits from Animal, which inherits from Life, then Pig has access to all the members of all those classes. Every object in Dart inherits, eventually, from Object, as illustrated in figure 2.2.

Figure 2.2. Object-oriented inheritance example

2.5.4. Factories and named constructors

Right now, one of the great challenges that humans are facing is creating renewable energy. There’s a common thread between all the possible sources of energy: it all ends up as energy in the end. But for a brief moment, the wind is just wind, not yet turned into energy; and sunbeams are just sunbeams. Science needs to know how to turn these different substances into energy.

Tip

I don’t know anything about physical science. Please go easy on me.

This is what factory and named constructors do. They’re special constructor methods of classes that create an instance of that class, but with predetermined properties. Named constructors always return a new instance of a class. factory methods have a bit more flexibility: they can return cached instances or instances that are subtypes. In code, that Energy class might look like this:

class Energy {
  int joules;

  Energy(this.joules);                                             1

  Energy.fromWind(int windBlows) {                                 2
    final joules = _convertWindToEnergy(windBlows);
    return  Energy(joules);                                        3
  }

  factory Energy.fromSolar(int sunbeams) {                         4
    if (appState.solarEnergy != null) return appState.solarEnergy;
    final joules = _convertSunbeamsToJoules(sunbeams);
    return appState.solarEnergy =  Energy(joules);
  }
}

  • 1 Default constructor
  • 2 Using “Energy.” syntax and returning an instance of that class makes this a named constructor.
  • 3 All constructors must return an instance of the class.
  • 4 The factory will potentially return an existing instance of Energy. Otherwise, it will create a new instance, assign it, and return it.

2.5.5. Enumerators

Enumerators, often called enums, are special classes that represent a specific number of constants. Suppose you have a method that takes a String and then does some magic and changes the color of text in an app:

void updateColor(String color) {
  if (color == 'red') {
    text.style.color = 'rgb(255,0,0)';
  } else if (color == 'blue') {
    text.style.color = 'rgb(0,0,255)';
  }
}

This is great, unless you pass “macaroni,” “crab cakes,” “33445533,” or any other string into updateColor. You can use an enum to buy yourself some type safety without the verbosity of a class. At the end of the day, that’s what an enum is about: it makes your code harder to break and easier to read.

So your Color enum can look like this:

enum Color { red, blue }

In your code, you can access colors like this: Color.red. Variables and fields can now have Color as a type, and it must be assigned to either Color.red or Color.blue. And as an added bonus, switch statements can switch on an enum and demand that you have a case statement for every type in the enum (or a default at the end).

Now your function can look like this:

enum Color { red, green, blue }

void updateColor(Color color) {
  switch(color) {
    case Color.red:
      // do stuff
    case Color.green:
      // do stuff
    case Color.blue:
      // do stuff
  }
}

Then, when you call the function, it must be passed a Color:

updateColor(Color.red);
updateColor(Color.green);
updateColor(Color.blue);

If you try to pass in “macaroni,” the code will throw an error.

A note about more Dart features

This chapter is meant to be an overview of the crucial pieces of Dart that you’ll need to write Flutter apps—but there’s much more. Some of the features of Dart that will be discussed later in this book are asynchronous features, type generics, abstract classes (also known as interfaces), and generator functions. These features are cool and important but need a lot of context to be described accurately. So, I will discuss them in depth when the time comes. For now, the only requirement is that you understand the foundation of Dart.

Summary

  • Dart’s syntax is familiar if you know any C-like language.
  • Dart is an object-oriented, strictly typed language.
  • All Dart programs begin with a main function as the entry point to the application.
  • Types are used to ensure that code is using the correct values at the correct time. They can seem cumbersome, but they’re helpful for reducing bugs.
  • Functions must return types or void.
  • Most operators in Dart are like operators in other languages, but there are a few special operators, such as ~/, is, and as.
  • Null-aware operators are useful for performing null checks, which ensure that values are not null.
  • For control flow, Dart supports if/else statements, as well as switch statements and ternary operators.
  • Using an enum with a switch statement enforces accounting for all possible cases.
  • Loops in Dart should be familiar if you come from most other languages. There are for loops, for-in loops, while loops, and do while loops.
  • Dart functions are objects and can be passed around like any other value. This is called a higher-order function in many languages.
  • Dart is a true object-oriented programming language, and your code will make heavy use of classes, constructors, and inheritance.
  • There are multiple types of constructors: the default constructor, factory constructors, and named constructors.
  • An enum is a special kind of class that gives additional type safety when there is a predetermined number of options for a property or variable.
..................Content has been hidden....................

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