Introduction to Dart 3.x

Dart was intended to be a language to supplant JavaScript on the web. Dart made up for a lot of the shortcomings of JavaScript including static types and recently better null protection. Microsoft beat Google to the punch and its use of TypeScript on the web has proliferated a large portion of web-based applications.

Dart would have died if Flutter didn’t save it from its fate and chances are if you are here reading this blog post you are more interested in building Flutter applications than running Dart on the web. Don’t get me wrong, Dart has amazing features which personally I would have loved in other languages including Just-In-Time compilation to help with it’s lightning fast reloads when debugging.

During this introduction, take some time to explore and experiment with the various features of the language using DartPad. It is a user-friendly and freely accessible platform that enables you to promptly receive feedback as you test out different aspects of the language.

Data Types

The declaration of a data type is pretty much similar syntactically to most languages including JavaScript and TypeScript. The basic structure of the declaration is as follows:

type identifier = value;

In Dart the primitive/built-in data types are:

  • Numbers (int, double)

  • Strings (String)

  • Booleans (bool)

  • Records ((value1, value2))

  • Lists (List, also known as arrays)

  • Sets (Set)

  • Maps (Map)

  • Runes (Runes; often replaced by the characters API)

  • Symbols (Symbol)

The identifier or variable name has a few rules:

  • Special characters such as underscores, exclamation marks and spaces are not allowed in variable names

  • Must not be preceded by a number

  • keywords are not allowed

Type Inference

Dart, despite being a statically typed language, allows for some type inference. This can be seen in the code block below:

var value = "My Name is John";
print(value);  // outputs -> My Name is John

This is perfectly acceptable and the compiler will make accomodations for the value variable to be treated as a String.

var value = "My Name is John";
print(value);
  
value = 20;

If we attempt to assign a different type value to the previously assigned string the compiler will respond with the following error:

compileDDC
main.dart:5:11: Error: A value of type 'int' can't be assigned to a variable of type 'String'.
  value = 20;
          ^

Variables

Variables store references and a fundamental to all programming languages. These references are integral to making decisions within your app.

Null Safety

Dart protects developers from themselves. The dart compiler is able to detect and stop compilation in the event that an expression may evaluate to null. Ask any JavaScript developer; one of the worst things to happen is a null reference during runtime. Dart enables null safety with 3 main changes:

  • Explicitly specifying that a variable is “nullable”. This means if a value has the potential to evaluate to null, then it is nullable. This is accomplished by adding a ? character to the end of the type declaration.
    String? name // name can be null or string

  • Variables must be initialized before using them.

  • Calls to an object’s methods cannot be made with a nullable type.

If a variable is nullable there will be additional work expected of the developer when attempting to access the value. Examine the following function:

void main() {
  int? value;
  print(value + 2);
}

The compiler would emit the following error during compilation:

compileDDC
main.dart:3:15: Error: Operator '+' cannot be called on 'int?' because it is potentially null.
  print(value + 2);

As good practice, it forces developers to check first if the value is null before attempting an operation with a null value. To fix this issue the following adjustments are made:

void main() {
  int? value;
  if(value != null){
    print(value + 2);
  }
}

Late variables

In many cases, a variable needs to be set later after being declared. Some of these situations include:

  • Initialization is expensive and takes some time/effort

  • Value is unavailable at the time it is declared

Dart accommodates these cases by allowing developers to use the keyword late and tell the compiler to initialize when needed. Example:

// This is the program's only call to readThermometer().
late String temperature = readThermometer(); // Lazily initialized.

In the example, if the temperature variable is never used, then the expensive readThermometer() function is never called.

final and const

For the Java Developers amongst us, these concepts should be familiar. For variables that never change values we have two options in Dart:

  • final allows us to set a variable to a value once and only once.
    final name = “Bob”; // Without a type annotation (implicit type of String)
    final String nickname = “Bobby”; // With a type
    If we attempted to set the values above after the first initialization the compiler will fail.

  • const allows us to set a variable to a value during compilation and are therefore implicitly final.
    const bar = 1000000; // Unit of pressure (dynes/cm2)
    const double atm = 1.01325 * bar; // Standard atmosphere

Data Structures

Most programming languages support a set of basic, but powerful constructs that allow for the temporary storage of information that is intended to be used to manipulate its contents and later present the details.

Lists

Each language presents their own implementation of some collection of ordered objects. In the case of Dart, we introduce lists. They are denoted by a set of square brackets []:

var list = [100, 122, 334];
print(list.length);		// prints 3
print(list[1]);			// prints 122
list[1] = 500;			// assigns the value 500 to the item in position 1
print(list[1]);			// prints 500

Note:

var list = [100, 122, 334]; is inferred to be a list of integers! You won’t be able to add a string to this list.

Items in a list are ordered based on their position, starting with 0. So in the example above, the item at position 0 is 100. Items can be inserted into the list by using the add method and removed by using the remove method. Example:

var list = [100, 122, 334];
print(list.length);			// prints 3
list.add(567);
print(list.length);			// prints 4
print(list[3]);				// prints 567
list.remove(100);
print(list);				// prints [122, 334, 567]

Sets

A set in Dart is an unordered collection.

var categories = {'Programming', 'Life Lessons', 'Life Hacks', 'Shopping'};

Set<String> locations = {};
locations.add('Kingston');
locations.addAll({'Miami', 'Port of Spain'});
print(categories);		// prints {Programming, Life Lessons, Life Hacks, Shopping}
print(locations);		// prints {Kingston, Miami, Port of Spain}
print(locations.length);	// prints 3

Maps

A map is a collection of key-value pairs where the key appears only once in the collection. It allows us to store associations between a key and a value, while providing convenient methods.

Map <int, String> studentRecords = {};
studentRecords[12939] = "John Doe";
studentRecords[33312] = "Mary Jane";
studentRecords[11112] = "Clark Kent";
print(studentRecords[12939]);		// prints 'John Doe'
print(studentRecords.length);		// prints 3
studentRecords.remove(33312);
print(studentRecords);			// prints {12939: John Doe, 11112: Clark Kent}

Control Logic

In any language the ability to control the flow of operations based on your needs is essential. Dart, like most modern languages, has features that are quite standard in this regard. This includes branching and looping and they allow for the manipulation of the flow of the program within your code.

Looping

At times we need to repeat a set of operations and these language structures allow for this behaviour in a controlled way.

For Loops

There are a few ways of accomplishing a for loop. The following is the most common:

for (initilizer; condition; incrementer) {
  // operations
}
  • The initializer portion of the for loop takes the variable(s) that are to be initialized to manage the loop.

  • The condition is what tells the code to continue if it evaluates to true. The code will end when the condition evaluates to false.

  • the incrementer allows us to change a value on each iteration of the loop

So to make sense of this, the follow example of making code count from 1 to 100 is shown:

for(int i = 0;i < 100; i++) {
    print(i+1);
}

Another common use of a for loop is to perform an operation on a collection of items. The syntax is slightly different from the previous example:

for(final item in collection) {
    // perform operations on item
}

In the syntax above:

  • item is an individual item within a list where all items are homogenous

  • collection is a Iterable type like a List or Set

In practice the for loop will look something like this:

for (final candidate in candidates) {
  candidate.interview();
}

While and Do-While Loops

A while loop iterates, but evaluates a condition before the loop:

int i = 0;
while(!bin.isFull()) {
    bin.add(i++);
}

A do-while loop evaluates the condition after the loop:

do {
  printLine();
} while (!atEndOfPage());

Branching

This includes if and switch statements that allow for some operations to happen if some criteria is met. The if statement takes the following format:

if (condition) {
    // do something
} else {
    // do something else
}

The condition section of the statement above is any logical expression that evaluates to true or false. These are examples of an if statement.

if (number < 0) {
    print("Number is negative");
} else if (number < 100) {
    print("Number is positive but less than 100");
} else {
    print("Number is positive but greater or equal to 100");
}

The if statement also has a shorter form of being written, take note of the absence of curl brackets:

if (isRaining())
    closeWindows();

switch Statements evaluates a value against a number of cases. If a case matches the value held in the variable then it will execute specific section of code:

switch(value) {
    case value1:
        // doSomething();
        break;
    case value2:
        // doSomethingElse();
        break;
    default:
        // doSomethingIfNoMatch();
        break;
}

In the syntax above, you will notice the use of break. This tells the computer to exit the switch statement and continue executing the code outside of the switch block. The absence of the break statement will cause the computer to continue to execute the code for subsequent cases.

Also take note of the default case. This allows us to accomodate for values that are not accounted for, such as unexpected values or unknown cases. The code below demonstrates a use-case of a robot of some kind being told to walk:

var opCode = "WALK";
switch (opCode) {
    case "KNEEL":
        executeKneel();
        break;
    case "WALK":
        executeWalk();
        break;
    case "SIT":
        executeSit();
        break;
    case "SLEEP":
           executeSleep();
           break;
    default:
        executeConfused();
        break;
}

break and continue

Let’s also showcase the use of break outside of switch statements. Breaks are useful to interrupt the continued flow of code. It can be used to jump out of loops such as the example below:

while(true) { // continues indefinitely
    if(shutDownRequested()) {
        break;
    } else {
        processIncomingRequests();
    }
}

This allows the computer to continue processing incoming requests until it receives some shutdown request at which point the computer will exit the loop.

Consider the case where a system must interview all candidates that have 5 or more years experience for a particular job. The system is given a list of candidates and it must interview each within the list that matches the criteria:

for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}

In this case if the candidate has less than 5 years, the computer will not execute the candidate.interview(); portion of the code and will instead continue and skip to the next loop iteration.

Functions

A function can be thought of as a block of code that solves a specific task. To simplify, it helps make code easier to read and manage by keeping it neat, so developers can concentrate on specific code sections. There are several variations of functions including anonymous functions, but we will touch on a few basic ideas.

void main() {
    print(isEven(2));
}
bool isEven(int number) {
    return (number % 2 == 0);
}

In the example above there are two functions, main and isEven. You will observe that the main function has the keyword void preceding the function name. This tells the compiler that the function returns nothing. The isEven function though has bool preceding the function name. This tells the compiler that the function returns a boolean type value.

A function must have a return type defined.

The second thing to observe are the items in the parentheses. The main function has no “parameters” and the isEven function has one “parameter” called number of type int.

A function can have any number of parameters.

The third and final observation with this example is that in the isEven function the keyword return makes an appearance. It tells the compiler that it must return the result of evaluating the proceeding statement. In the example the statement is a comparison that checks if the number leaves no remainder after being divided by two then it evaluates to true.

If the function has a return type other than void, then the return statement must return a value of that type.

An application will always have reference to the main() function. It is the entrypoint to the application and kicks off all other processes within your app and always returns void.

bool isEven(int number) => (number % 2 == 0);

The line above is actually another way of writing the same function previously described. It is a shorthand form referred to as arrow syntax that makes writing very small functions a little shorter and can be more convenient.


Named Parameters

void setText({bool? bold, bool? italics, bool? underlined}) {
    // does something
}

The function setText has a different set of parameters in comparison to the examples seen in previous examples. These are called named parameters and they are optional unless explicitly marked as required. You will notice that the type indicator has a question mark to indicate they may be null as they are optional. To call the function and specify the arguments you can do the following:

setText(bold: false, underlined: true);  // bold = false, italics = null and underlined = true

To tell the compiler a default value instead of handling potential null values, you can do the following in the parameters definition:

void setText({bool bold = false, bool italics = false, bool underlined = false}) {
    // does something
}

setText(bold: true); // bold = true, italics = false and underlined = false

What if you want to force your developers to add a named parameter and supply a value? We have the following option, note however that in the example, language can still be null:

const setLanguage({required String? language}){
    ...
}

Positional Parameters

The positional parameters can also be marked as optional. Consider the following example:

String say(String from, String msg, [String? device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

In this example, it allows us to call the function say in any of the following ways:

say("Johnny","I am Johnny Five and I am alive");
say("Mark", "Hello from the other side!", "Android Phone");
Next
Next

Designing Backend APIs in One Shot: Best Practices for Efficiency and Maintainability