Dart Apprentice — Part II

Hamed Seify
CodeX
Published in
22 min readAug 21, 2021

Since softwares are growing everyday, developers need higher level of programming knowledge to provide better solutions in less time. In this part I’ll present rest of the Dart Apprentice book to you that will include collections, functions and classes in Dart.

If you are not familiar with basic of dart programming, I suggest you to first read the part one in below link and then continue part two:

In the following I’m going to introduce functions as a way to write a code once and use many times, collection as a kind of variables that can hold a batch of a special type, and classes as a blueprint of new types. Prepare a glass of delicious drink and start.

Functions:

A function is one small task, or sometimes a collection of several smaller, related tasks that you can use in conjunction with other functions to accomplish a larger task. You can think of functions like machines; they take something you provide to them (the input), and produce something different (the output).

There are many examples of this in daily life. With an apple juicer, you put in apples and you get out apple juice. The input is apples; the output is juice.

Anatomy of a Dart function:

In Dart, a function consists of a return type, a name, a parameter list in parentheses and a body enclosed in braces.

Here is a short summary of the labeled parts of the function:

  • Return type: This comes first; it tells you immediately what the type will be of the function output. This particular function will return a String , but your functions can return any type you like. If the function won’t return anything, that is, if it performs some work but doesn’t produce an output value, you can use void as return type.
  • Function name: You can name functions almost anything you like, but you should follow the lowerCamelCase naming convention. You’ll learn a few more naming conventions a little later in this chapter.
  • Parameters: Parameters are the input to the function; they go inside the parentheses after the function name. This example has only one parameter, but if you had more than one, you would separate them with commas. For each parameter, you write the type first, followed by the name. Just as with variable names, you should use lowerCamelCase for your parameter names.
  • Return value: This is the function’s output, and it should match the return type. In the example above, the function returns a String value by using the return keyword. If the return type is void , though, then you don’t return anything. The return type, function name and parameters are collectively known as the function signature. The code between the braces is known as the function body.

This is what the function above looks like in the context of a program:

void main() {
const input = 12;
final output = compliment(input);
print(output);
}
String compliment(int number) {
return '$number is a very nice number.';
}

Run the code now and you’ll see the following result:

12 is a very nice number.

More about parameters

Parameters are incredibly flexible in Dart, so they deserve their own section.

Using multiple parameters

In a Dart function, you can use any number of parameters. If you have more than one parameter for your function, simply separate them with commas. Here’s a function with two parameters:

void helloPersonAndPet(String person, String pet) {
print('Hello, $person, and your furry friend, $pet!');
}

Parameters like the ones above are called positional parameters, because you have to supply the arguments in the same order that you defined the parameters when you wrote the function. If you call the function with the parameters in the wrong order, you’ll get something obviously wrong:

helloPersonAndPet('Fluffy', 'Chris');
// Hello, Fluffy, and your furry friend, Chris!

Making parameters optional

To indicate that a parameter is optional, you surround the parameter with square brackets and add a question mark after the type, like so:

String fullName(String first, String last, [String? title]) {
if (title != null) {
return '$title $first $last';
} else {
return '$first $last';
}
}

Writing [String? title] makes title optional. If you don’t pass in a value for title , then it will have the value of null , which means “no value”. Here are two examples to test it out:

print(fullName('Ray', 'Wenderlich'));
print(fullName('Albert', 'Einstein', 'Professor'));

Run that now and you’ll see the following:

Ray Wenderlic
Professor Albert Einsteinh

Providing default values

You can even use initializing values for parameters in function signature via declaring parameters with a default value. Take a look at this example:

bool withinTolerance(int value, [int min = 0, int max = 10]) {
return min <= value && value <= max;
}

There are three parameters here, two of which are optional: min and max . If you don’t specify a value for them, then min will be 0 and max will be 10. Here are some specific examples to illustrate that:

withinTolerance(5) // true
withinTolerance(15) // false

Since 5 is between 0 and 10 , this evaluates to true ; but since 15 is greater than the default max of 10 , it evaluates to false. If you want to specify values other than the defaults, you can do that as well:

withinTolerance(9, 7, 11) // true

Since 9 is between 7 and 11 , the function returns true. If that wasn’t bad enough, the following function call also returns true :

withinTolerance(9, 7) // true

Since the function uses positional parameters, the provided arguments must follow the order you defined the parameters. That means value is 9 , min is 7 and max has the default of 10 . But who could ever remember that? maybe naming parameters help to read code easier.

Naming parameters

Dart allows you to use named parameters to make the meaning of the parameters more clear in function calls. To create a named parameter, you surround it with curly braces instead of square brackets. Here’s the same function as above, but using named parameters instead:

bool withinTolerance(int value, {int min = 0, int max = 10}) {
return min <= value && value <= max;
}

Note the following:

  • min and max are surrounded by braces, which means you must use the parameter names when you provide their argument values to the function.
  • Like square brackets, curly braces make the parameters inside optional. Since value isn’t inside the braces, though, it’s still required.

To provide an argument, you use the parameter name, followed by a colon and then the argument value. Here is how you call the function now:

withinTolerance(9, min: 7, max: 11); // true

An additional benefit of named parameters is that you don’t have to use them in the exact order in which they were defined. These are both equivalent ways to call the function:

withinTolerance(9, min: 7, max: 11); // true
withinTolerance(9, max: 11, min: 7); // true

And since named parameters are optional, that means the following function calls are also valid:

withinTolerance(5); // true
withinTolerance(15); // false
withinTolerance(5, min: 7); // false
withinTolerance(15, max: 20); // true

Making named parameters required;

What you want is to make value required instead of optional, while still keeping it as a named parameter. You can achieve this by including value inside the curly braces and adding the required keyword in front:

bool withinTolerance(required int value,int min = 0,int max = 10,}) {
return min <= value && value <= max;
}

With the required keyword in place, VS Code will warn you if you don’t provide a value for value when you call the function:

You can read more about functions, specially Anonymous functions and Arrow functions in book.

Classes

In part one, you’ve used built-in types such as int , String and bool. Now, you are ready to learn a more flexible way to create your own types by using classes.

Dart classes

Classes are like architectural blueprints that tell the system how to make an object, where an object is the actual data that’s stored in the computer’s memory. If a class is the blueprint, then you could say the object is like the house that the blueprint represents. For example, the String class describes its data as a collection of UTF-16 code units, but a String object is something concrete like ‘Hello, Dart!’.

Classes are a core component of object-oriented programming. They’re used to combine data and functions inside a single structure.

The functions exist to transform the data. Functions inside of a class are known as methods, while constructors are special methods you use to create objects from the class.

Defining a class

For defining a new class must use ‘class’ keyword then write name of the class and after that its stuffs in curly braces. Write the following simple class at the top level of your Dart file. Your class should be outside of the main function, either above or below it.

class User {
int id = 0;
String name = '';
}

This creates a class named User. It has two properties; id is an int with a default value of 0, and name is a String with the default value of an empty string.

Creating an object from a class

As mentioned above, the value you create from a class is called an object. Another name for an object is instance, so creating an object is sometimes called instantiating a class.

Since coding the class in your Dart file as you did above simply creates the blueprint, a User object doesn’t exist yet. You can create one by calling the class name as you would call a function. Add the following line inside the main function:

final user = User();

This creates an instance of your User class and stores that instance, or object, in user. Notice the empty parentheses after User. It looks like you’re calling a function without any parameters. In fact, you are calling a type of function called a constructor method. You’ll learn a lot more about them later in the chapter. Right now, simply understand that using your class in this way creates an instance of your class.

Assigning values to properties

Now that you have an instance of User stored in user, you can assign new values to this object’s properties by using dot notation. To access the name property, type user dot name, and then give it a value:

user.name = 'Ray';

Now, set the ID in a similar way:

user.id = 42;

Cascade notation

Dart offers a cascade operator (..) that allows you to chain together multiple assignments on the same object without having to repeat the object name. The following code is equivalent:

final user = User()
..name = 'Ray'
..id = 42;

Note that the semicolon only appears on the last line.

Constructors

Constructors are methods that create, or construct, instances of a class. That is to say, constructors build new objects. Constructors have the same name as the class, and the implicit return type of the constructor method is also the same type as the class itself.

Default constructor

As it stands, your User class doesn’t have an explicit constructor. In cases like this, Dart provides a default constructor that takes no parameters and just returns an instance of the class. For example, defining a class like this:

class Address {
var value = '';
}

Is equivalent to writing it like this:

class Address {
Address();
var value = '';
}

Including the default Address() constructor is optional.

Custom constructors

Like the default constructor above, the constructor name should be the same as the class name. This type of constructor is called a generative constructor because it directly generates an object of the same type.

Long-form constructor

Add the following generative constructor method at the top of the class body:

User(int id, String name) {
this.id = id;
this.name = name;
}

‘this’ is a new keyword. What does it do?

The keyword this in the constructor body allows you to disambiguate which variable you’re talking about. It means this object. So this.name refers the object property called name, while name (without this) refers to the constructor parameter. Using the same name for the constructor parameters as the class properties is called shadowing. So the constructor above takes the id and name parameters and uses this to initialize the properties of the object.

Short-form constructor

Dart also has a short-form constructor where you don’t provide a function body, but you instead list the properties you want to initialize, prefixed with the this keyword. Arguments you send to the short form constructor are used to initialize the corresponding object properties. Last snippet do exact same below snippet:

User(this.id, this.name);

Dart infers the constructor parameter types of int and String from the properties themselves that are declared in the class body.

Named constructors

Dart also has a second type of generative constructor called a named constructor, which you create by adding an identifier on to the class name. It takes the following pattern:

ClassName.identifierName()

Why would you want a named constructor instead of the nice, tidy default one? Well, sometimes you have some common cases that you want to provide a convenience constructor for. Or maybe you have some special edge cases for constructing certain classes that need a slightly different approach.

Say, for example, that you want to have an anonymous user with a preset ID and name. You can do that by creating a named constructor. Add the following named constructor below the short-form constructor:

User.anonymous() {
id = 0;
name = 'anonymous';
}

The identifier, or named part, of the constructor is .anonymous. Named constructors may have parameters, but in this case, there are none. And since there aren’t any parameter names to get confused with, you don’t need to use this.id or this.name. Rather, you just use the property variables id and name directly. Call the named constructor in main like so:

final anonymousUser = User.anonymous();
print(anonymousUser);

Static members

If you put static in front of a member variable or method, that causes the variable or method to belong to the class rather than the instance:

class SomeClass {
static int myProperty = 0;
static void myMethod() {
print('Hello, Dart!');
}
}

And you access them like so:

final value = SomeClass.myProperty;
SomeClass.myMethod();

In this case, you didn’t have to instantiate an object to access myProperty or to call myMethod. Instead, you were able to use the class name directly to get the value and call the method.

Collections

In almost every application you make, you’ll be dealing with collections of data. Data can be organized in multiple ways, each with a different purpose. Dart provides multiple solutions to fit your collection’s needs, and in this chapter you’ll learn about three of the main ones: lists, sets and maps.

Lists

Whenever you have a very large collection of objects of a single type that have an ordering associated with them, you’ll likely want to use a list as the data structure for ordering the objects. Lists in Dart are similar to arrays in other languages.

The image below represents a list with six elements. Lists are zero-based, so the first element is at index 0. The value of the first element is cake, the value of the second element is pie, and so on until the last element at index 5, which is cookie.

The order of a list matters. Pie is after cake, but before donut. If you loop through the list multiple times, you can be sure the elements will stay in the same location and order.

Creating a list

You can create a list by specifying the initial elements of the list within square brackets. This is called a list literal.

var desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];

Since all of the elements in this list are strings, Dart infers this to be a list of String types.

You can reassign desserts (but why would one ever want to reassign desserts?) with an empty list like so:

desserts = [];

Dart still knows that desserts is a list of strings. However, if you were to initialize a new empty list like this:

var snacks = [];

Dart wouldn’t have enough information to know what kind of objects the list should hold. In this case, Dart simply infers it to be a list of dynamic. This causes you to lose type safety, which you don’t want. If you’re starting with an empty list, you should specify the type like so:

List<String> snacks = [];

There are a couple of details to note here:

  • List is the data type, or class name, as you learned before.
  • The angle brackets < > here are the notation for generic types in Dart. A generic list means you can have a list of anything; you just put the type you want inside the angle brackets. In this case, you have a list of strings, but you could replace String with any other type. For example, List<int> would make a list of integers, List<bool> would make a list of Booleans, and List<Grievance> would make a list of grievances — but you’d have to define that type yourself since Dart doesn’t come with any by default.

A slightly nicer syntax for creating an empty list is to use var or final and move the generic type to the right:

var snacks = <String>[];

Accessing elements

To access the elements of a list, you reference its index via subscript notation, where the index number goes within square brackets after the list name.

final secondElement = desserts[1];
print(secondElement);

Don’t forget that lists are zero-based, so index 1 fetches the second element. Run that code and you’ll see cupcakes as expected.

If you know the value but don’t know the index, you can use the indexOf method to look it up:

final index = desserts.indexOf('pie')
final value = desserts[index];;

Since ‘pie’ is the fourth item in the zero-based list, index is 3 and value is pie.

Assigning values to list elements

Just as you access elements, you also assign values to specific elements using subscript notation:

desserts[1] = 'cake';

This changes the value at index 1 from cupcakes to cake.

Adding elements to a list

Lists are growable by default in Dart, so you can use the add method to add an element.

desserts.add('brownies');

Run that and you’ll see:

[cookies, cake, donuts, pie, brownies]

Removing elements from a list

You can remove elements using the remove method. So if you’d gotten a little hungry and eaten the cake, you’d write:

desserts.remove('cake');
print(desserts);

This leaves a list with four elements:

[cookies, donuts, pie, brownies]

List properties

Collections such as List have a number of properties. To demonstrate them, use the following list of drinks.

const drinks = ['water', 'milk', 'juice', 'soda'];

You can access the first and last element in a list:

drinks.first   // water
drinks.last // soda

You can also check whether a list is empty or not empty:

drinks.isEmpty      // false
drinks.isNotEmpty // true

This is equivalent to the following:

drinks.length == 0   // false
drinks.length > 0 // true

Looping over the elements of a list

For this section you can return to your list of desserts:

const desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];

In for loops part, you saw how to iterate over lists, so this is a review of the for-in loop.

for (var dessert in desserts) {
print(dessert);
}

Each time through the loop, dessert is assigned an element from desserts.

You also can use forEach function:

desserts.forEach((dessert) => print(dessert));

Sets

Sets are used to create a collection of unique elements. Sets in Dart are similar to their mathematical counterparts. Duplicates are not allowed in a set, in contrast to lists, which do allow duplicates.

Creating a set

You can create an empty set in Dart using the Set type annotation like so:

final Set<int> someSet = {};

The generic syntax with int in angle brackets tells Dart that only integers are allowed in the set. The following form is shorter but identical in result:

final someSet = <int>{};
final anotherSet = {1, 2, 3};

Operations on a set

To see if a set contains an item, you use the contains method, which returns a bool. Add the following two lines and run the code again:

print(anotherSet.contains(1)); // true
print(anotherSet.contains(99)); // false

Since anotherSet does contains 1, the method returns true, while checking for 99 returns false.

Like growable lists, you can add and remove elements in a set. To add an element, use the add method.

final someSet = <int>{};
someSet.add(42);
someSet.add(2112);
someSet.add(42);
print(someSet);

Run that to see the following set:

{42, 2112}

Notice that ‘42’ is added once. That’s because of set holds each item once and if you try to add repetitious item, set ignores it.

You can also remove elements using the remove method:

someSet.remove(2112);

Print someSet to reveal only a single element is left:

{42}

You can use addAll to add elements from a list into a set:

someSet.addAll([1, 2, 3, 4]);

Print someSet again to show the new contents:

{42, 1, 2, 3, 4}

Maps

Maps in Dart are the data structure used to hold key-value pairs. They’re similar to HashMaps and Dictionaries in other languages.

If you’re not familiar with maps, though, you can think of them like a collection of variables that contain data. The key is the variable name and the value is the data that the variable holds. The way to find a particular value is to give the map the name of the key that is mapped to that value.

In the image below, the cake is mapped to 500 calories, and the donut is mapped to 150 calories. cake and donut are keys, while 500 and 150 are values.

The key and value in each pair are separated by colons, and consecutive key-value pairs are separated by commas.

Creating an empty map

Like List and Set, Map is a generic type, but Map takes two type parameters: one for the key and one for the value. You can create an empty map variable using Map and specifying the type for both the key and value:

final Map<String, int> emptyMap = {};

In this example, String is the type for the key, and int is the type for the value. A slightly shorter way to do the same thing is move the generic types to the righthand side:

final emptyMap = <String, int>{};

You can create a non-empty map variable using braces, where Dart infers the key and value types. Dart knows it’s a map because each element is a pair separated by a colon.

final inventory = {
'cakes': 20,
'pies': 14,
'donuts': 37,
'cookies': 141,
};

Unique keys

The keys of a map should be unique. A map like the following wouldn’t work:

final treasureMap = {
'garbage': 'in the dumpster',
'glasses': 'on your head',
'gold': 'in the cave',
'gold': 'under your mattress',
};

There are two keys named gold. How are you going to know where to look? You’re probably thinking, “Hey, it’s gold. I’ll just look both places.” If you really wanted to set it up like that, then you could map String to List:

final treasureMap = {
'garbage': ['in the dumpster'],
'glasses': ['on your head'],
'gold': ['in the cave', 'under your mattress'],
};

Now every key contains a list of items, but the keys themselves are unique. Values don’t have that same restriction of being unique. This is fine:

final myHouse = {
'bedroom': 'messy',
'kitchen': 'messy',
'living room': 'messy',
'code': 'clean',
};

Operations on a map

You access individual elements from a map by using a subscript notation similar to lists, except for maps you use the key rather than an index.

final numberOfCakes = inventory['cakes'];

If you recall from above, the key cakes is mapped to the integer 20, so print numberOfCakes to see 20. A map will return null if the key doesn’t exist.

You can add new elements to a map simply by assigning to elements that are not yet in the map.

inventory['brownies'] = 3;

Print inventory to see brownies and its value at the end of the map:

{cakes: 20, pies: 14, donuts: 37, cookies: 141, brownies: 3}

Remember that the keys of a map are unique, so if you assign a value to a key that already exists, you’ll overwrite the existing value.

inventory['cakes'] = 1;

Print inventory to confirm that cakes was 20 but now is 1:

{cakes: 1, pies: 14, donuts: 37, cookies: 141, brownies: 3}

You can use remove to remove elements from a map by key.

inventory.remove('cookies');

and result:

{cakes: 1, pies: 14, donuts: 37, brownies: 3}

Map properties

Maps have properties just as lists do. For example, the following properties indicate (using different metrics) whether or not the map is empty:

inventory.isEmpty     // false
inventory.isNotEmpty // true
inventory.length // 4

You can also access the keys and values separately using the keys and values properties.

print(inventory.keys);
print(inventory.values);

When you print that out, you’ll see the following:

(cakes, pies, donuts, brownies)
(1, 14, 37, 3)

To check whether a key is in a map, you can use the containsKey method:

print(inventory.containsKey('pies'));
// true

You can do the same for values using containsValue.

print(inventory.containsValue(42));
// false

Looping over elements of a map

The keys and values properties of a map are iterables, so you can loop over them. Here’s an example of iterating over the keys:

for (var item in inventory.keys) {
print(inventory[item]);
}

You can also use forEach to iterate over the elements of a map, which gives you both the keys and the values.

inventory.forEach((key, value) => print('$key -> $value'));

And this for loop does the same thing:

for (final entry in inventory.entries) {
print('${entry.key} -> ${entry.value}');
}

Running either loop gives the following result:

cakes -> 1
pies -> 14
donuts -> 37
brownies -> 3

Mapping over a collection

Mapping over a collection allows you to perform an action on each element of the collection as if you were running it through a loop. To do this, collections have a map method that takes an anonymous function as a parameter, and returns another collection based on what the function does to the elements. Write the following code:

const numbers = [1, 2, 3, 4];
final squares = numbers.map((number) => number * number);

Inside the anonymous function body, you’re allowed to do whatever you want. In the case above, you square each input value. Print squares to see the result:

(1, 4, 9, 16)

Filtering a collection

You can filter an iterable collection like List and Set down to another shorter collection by using the where method. Add the following line below the code you already have:

final evens = squares.where((square) => square.isEven);

The function’s input is also each element of the list, but unlike map, the value the function returns must be a Boolean. If the function returns true for a particular element, then that element is added to the resulting collection, but if false, then the element is excluded. Using isEven makes the condition true for even numbers, so you’ve filtered down squares to just the even values. Print evens and you’ll get:

(4, 16)

Sorting a list

Calling sort on a list sorts the elements based on their data type.

final desserts = ['cookies', 'pie', 'donuts', 'brownies'];
desserts.sort();

Print desserts and you’ll see the following:

[brownies, cookies, donuts, pie]

Since desserts holds strings, calling sort on the list arranges them in alphabetical order. The sorting is done in place, which means sort mutates the input list itself. This also means if you tried to sort a const list, you’d get an error.

You can use reversed to produce a list in reverse order.

var dessertsReversed = desserts.reversed;

This produces the following result:

(pie, donuts, cookies, brownies)

Advanced Classes

In many situations, you’ll need to create a hierarchy of classes that share some base functionality. You can create your own hierarchies by extending classes. This is also called inheritance, because the classes form a tree in which child classes inherit from parent classes. The parent and child classes are also called super classes and subclasses respectively.

Creating your first subclass

Here we have a Person class that have two fields givenName and surname:

class Person {
Person(this.givenName, this.surname);
String givenName;
String surname;
String get fullName => '$givenName $surname';
@override
String toString() => fullName;
}

Then we add Student class that has same fields as Person, plus a grade field:

class Student {
Student(this.givenName, this.surname);
String givenName;
String surname;
String grade = 'F';
String get fullName => '$givenName $surname';
@override
String toString() => fullName;
}

You can remove the duplication between Student and Person by making Student extend Person. You do so by adding extends Person after the class name, and removing everything but the Student constructor and the grade. Replace the Student class with the following code:

class Student extends Person {
Student(String givenName, String surname)
: super(givenName, surname);
String grade = 'F';
}

There are a few points to pay attention to:

  • The constructor parameter names don’t refer to this anymore. Whenever you see the keyword this, you should remember that this refers to the current object, which in this case would be an instance of the Student class. Since Student no longer contains the field names givenName and surname, using this.givenName or this.surname would have nothing to reference.
  • In contrast to this, the super keyword is used to refer one level up the hierarchy. Similar to the forwarding constructor that you learned about in above, using super(givenName, surname) passes the constructor parameters on to another constructor. However, since you’re using super instead of this, you’re forwarding the parameters to the parent class’s constructor, that is, to the constructor of Person.

Using the classes

OK, back to the primary example. Create Person and Student objects like so:

final jon = Person('Jon', 'Snow');
final jane = Student('Jane', 'Snow');
print(jon.fullName);
print(jane.fullName);

Run that and observe that both have full names:

Jon Snow
Jane Snow

Overriding parent methods

Suppose you want the student’s full name to print out differently than the default way it’s printed in Person. You can do so by overriding the fullName getter. Add the following two lines to the bottom of the Student class:

@override
String get fullName => '$surname, $givenName';

‘@override’ tell to compiler that use this implementation instead of parents function. While using @override is technically optional in Dart, it does help in that the compiler will give you an error if you think you’re overriding something that doesn’t actually exist in the parent class.

Run the code now and you’ll see the student’s full name printed differently than the parent’s.

Jon Snow
Snow, Jane

Conclusion

Thank you for taking the time to read this article. I will try to help you become more familiar with the Dart programming language. As I mentioned before, this book is a summary of the original book for people who want to quickly scan the book because of the lack of time. Please take the time to read the book if you have enough time to learn deeper topics. Thank you for your encouragement through the likes and I look forward to your helpful comments.

--

--