Skip to content

Dart Introduction

Dart is a modern programming language developed by Google, known for its simplicity and efficiency, especially in the context of web and mobile app development. Let's start with the basics 😎

  • Purpose and Usage: Dart is primarily used for building web and mobile applications. It's the language behind Flutter, Google's UI toolkit for crafting natively compiled applications for mobile, web, and desktop from a single codebase.
  • Syntax Similarities: Dart's syntax is similar to other C-style languages (like Java and JavaScript), so it's relatively easy to learn if you have experience with these languages πŸ§™πŸΌβ€β™‚οΈ

Play with dart

Choice 1 - Set Up Flutter To work with Flutter, adhere to the instructions for Flutter setup.

Choice 2 - Set Up Dart If Flutter isn't your focus, refer to the Dart SDK Installation guide.

Choice 3 - Go for Dart Pad For browser-based practice, DartPad is a good option.

Dart CLI

Here's an expanded explanation of some common commands:

Creating a New Dart Project

dart create -t console-simple my_app

This command initializes a new Dart project with a simple console template. Replace my_app with your project name. It creates a new directory with the project name and sets up the necessary files.

Running Your Application

dart run

This command is used to run your Dart application. It compiles the Dart code to native code and executes it. This is the command you'll use most frequently while developing to test your code.

Compiling Dart code to an exe

dart compile exe bin/dart.dart

This command compiles your Dart code (located in bin/dart.dart) into a standalone executable file. The generated executable (dart.exe in this case) can be run on any machine without needing the Dart SDK installed. This is useful for distributing your application.

Fundamental Data Types in Dart

In Dart, you specify a variable's type before its name when declaring it.

// inside your main.dart file 
int num1 = 2;
double num2 = 3.0;
bool isTrue = true;
String str = 'Hello';

Checking a Variable's Type at Runtime

To determine a variable's type during program execution, use the is keyword or the runtimeType property.

// inside your main.dart file 
(num1 + num2) is int
(num1 + num2).runtimeType

Using the var keyword

Declaring a variable with var implies no specific type annotation. If not initialized, Dart treats it as dynamic, but it's better to specify the type explicitly.

// inside your main.dart file 
var username; // Becomes dynamic
var username = 'yeeeah'; // Inferred as String
'final' versus 'const'

Use final to define a variable that you won't reassign. This is a recommended practice.

final String fullname = 'Jonh';
fullname = 'Jonh' // Causes an error

const is similar to final but for compile-time constants, which means the value must be known during compilation. This can reduce performance so be carful !

const int age = 75;
const int favNumber = num1 + 5; // Causes an error, not a compile-time constant

For additional information, refer to Dart's official documentation.

Null safety

Dart 2.0 introduces sound null safety, a feature that changes how null values are handled. With this, variables are not null by default, reducing runtime errors and eliminating the need for frequent null checks, thus streamlining the code.

By default, variables are non-nullable, meaning they cannot hold null values. Trying to assign null to such a variable triggers a compile-time error.

int age = 75; // Non-nullable
int age = null; // This will cause an error

Allowing Null values

To explicitly allow a variable to hold a null value, append a question mark ? to its type.

int? age; // Now nullable

Late Initialization

There are scenarios where a variable cannot be initialized immediately, but you are certain it will be assigned later during runtime. To handle this, use the late keyword. This is for 'lazy' initialization and should be used cautiously.

// main.dart
class Thing {
  late final String _size;

  void up() {
    _size = 'up';
    print(_size);
  }
}

The Assertion Operator

In situations where you want to assign a nullable value to a non-nullable variable, Dart prohibits this by default. However, you can override this with the assertion operator !, which tells the compiler the value is non-null.

String? answer;
String result = answer; // This results in an error
String result = answer!; // This works, using the assertion operator

This quick overview of sound null safety in Dart 2.0 highlights how the language handling null values more efficiently.

Dart Operators

Dart includes several operators that add interesting capabilities to your code.

The Assignment Operator

This operator assigns a value to a variable, but only if the variable currently has no assigned value.

String? name;
name ??= 'Guest'; // Assign 'Guest' if name is null
var z = name ?? 'Guest'; // z gets the value of name, or 'Guest' if name is null

Ternary Operator

The ternary operator offers a concise way to implement an if/else condition.

String color = 'blue';
var isThisBlue = color == 'blue' ? 'Yep, blue it is' : 'Nah, it's not blue';

Cascade Operator

This operator allows you to perform multiple operations on the same object without repeating its name. It's particularly handy for streamlining code, like when building Flutter’s widget tree.

// Instead of:
// var paint = Paint();
// paint.color = 'black';
// paint.strokeCap = 'round';
// paint.strokeWidth = 5.0;

// Use cascade:
var paint = Paint()
    ..color = 'black'
    ..strokeCap = 'round'
    ..strokeWidth = 5.0;

Typecasting

Sometimes, you might need to explicitly convert a value from one type to another.

var number = 23 as String; // Casts the integer 23 to a String
number is String; // Returns true, confirming the typecast

These operators in Dart enhance the language's flexibility and allow for more expressive and concise code.

Functions

Function with positional parameters:

  // Basic Function
  String takeFive(int number) {
    return '$number minus five equals ${number - 5}';
  }

Function with named parameters

// Named parameters
namedParams({required int a, int b = 5}) {
  return a - b;
}
namedParams(a: 23, b: 10);

Arrow Functions

Arrow functions are useful when passing functions as parameters to other functions.

// Arrow Function
String takeFive(int number) => '$number minus five equals ${number - 5}';

Callback Functions

Many APIs in Dart use callback functions, often to handle events or gestures in Flutter.

// First-class functions
callIt(Function callback) {
  var result = callback();
  return 'Result: $result';
}

Basic Lists

Like every other programming langage dart have list type.

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

list[0]; // 1
list.length; // 5
list.last; // 5
list.first; // 1

Loops

Let's see how to loop through a list in dart with the traditional method and the forEach method like this :

for (int n in list) {
  print(n);
}

list.forEach((n) => print(n));

//apply change directly on the list with map function 
var doubled = list.map((n) => n * 2);

Spread Syntax and conditions

var combined = [...list, ...doubled];
combined.forEach(print);

bool depressed = false;
var cart = [
    'milk', 
    'eggs', 
    if (depressed) 'Vodka'
];

Maps in Dart

In Dart, a map is a collection of key-value pairs, where each key is unique. Maps are often used to represent structured data, similar to dictionaries in Python or objects in JavaScript.

Creating and Using Basic Maps To define a map, specify the types of its keys and values, followed by its pairs enclosed in curly braces {}. Here's an example:

// maps.dart
Map<String, dynamic> book = {
  'title': 'Moby Dick',
  'author': 'Herman Melville',
  'pages': 752,
};

book['title']; // Accesses the value associated with the key 'title'
book['published'] = 1851; // Adds a new key-value pair to the map

In the example, a map book is created with string keys and dynamic values, meaning the values can be of any type.

Iterating Over a Map

Dart provides several ways to loop through a map, allowing you to access its keys, values, or both.

Accessing Keys and Values Separately:

  • book.keys: Retrieves all the keys from the map.
  • book.values: Retrieves all the values.
  • book.values.toList(): Converts the map's values to a list.

A MapEntry object represents a key-value pair. The following loop prints each key-value pair in the map:

// maps.dart
for (MapEntry b in book.entries) {
  print('Key ${b.key}, Value ${b.value}');
}

Or we can also use the forEach method is another way to iterate over a map. It takes a function that is applied to each key-value pair:

// maps.dart
book.forEach((k, v) => print("Key : $k, Value : $v"));

Classes and Objects in Dart

In Dart, classes are fundamental constructs used to create objects. They act as blueprints that define the properties and behaviors of objects.

Creating a Class

As you may know a class encapsulates data and functions. It can hold variables (properties) and functions (methods) that define the characteristics and actions of an object.

// classes.dart
class Basic {
  int id; // Property

  Basic(this.id); // Constructor

  doStuff() { // Method
    print('Hello my ID is $id');
  }
}

In this example, Basic is a class with a property id, a constructor to initialize that property, and a method doStuff that performs an action.

Instantiating an object

Creating an instance of a class (an object) is known as instantiation. The new keyword is optional in Dart.

// classes.dart
Basic thing = new Basic(55); // 'new' is optional
thing.id; // Access property
thing.doStuff(); // Call method

Here, thing is an object of the class Basic, created with the ID 55. We can access its properties id and call its methods doStuff.

Using Static Methods

Static methods and properties belong to the class itself rather than to instances of the class. They can be accessed directly using the class name.

// main.dart
class Basic {

  static globalData = 'global'; // Static property
  static helper() { // Static method
      print('helper');
  }
}

Basic.globalData; // Accessing static property
Basic.helper(); // Calling static method

In this context, globalData and helper are static. They are associated with the class Basic and can be accessed without creating an instance of Basic.

Constructors in Dart

Constructors in Dart are special functions used to initialize objects. They can be configured in various ways for flexible object creation.

Basics of Constructors

The this keyword refers to the current instance of a class. It's typically used in constructors to distinguish between class properties and constructor parameters, especially in cases of name collision.

// constructors.dart
class Rectangle {
  final int width;
  final int height;
  String? name;
  late final int area;

  Rectangle(this.width, this.height, [this.name]) {
    area = width * height; // Calculating area during object initialization
  }
}

In this Rectangle class, this.width and this.height are used to assign values to the properties from the constructor parameters. The optional parameter [this.name] demonstrates optional arguments, and area is calculated and assigned a value within the constructor.

Named Parameters

Named parameters enhance readability and flexibility in Dart, and they are extensively used in Flutter for widget creation.

// constructors.dart
class Circle {
  const Circle({required int radius, String? name});
}

const cir = Circle(radius: 50, name: 'foo'); // Creating instance with named parameters

Here, Circle is defined with named parameters. The required keyword indicates that radius must be provided, while name is optional.

Named Constructors

Dart allows classes to have multiple named constructors. This is especially useful for initializing the same class in different ways.

// constructors.dart
class Point {
  double lat = 0;
  double lng = 0;

  // Named constructor from a Map
  Point.fromMap(Map data) {
    lat = data['lat'];
    lng = data['lng'];
  }

  // Named constructor from a List
  Point.fromList(List data) {
    lat = data[0];
    lng = data[1];
  }
}

var p1 = Point.fromMap({'lat': 23, 'lng': 50});
var p2 = Point.fromList([23, 50]);

In the Point class, there are two named constructors: Point.fromMap and Point.fromList. Each constructor initializes the object differently, based on whether the input is a Map or a List.

Interfaces

In Dart, an interface is a blueprint for classes, defining a set of methods and properties that implementing classes must have. Additionally, Dart uses access modifiers to control the visibility of class members.

An interface in Dart is implicitly defined by a class. Any class can act as an interface, and other classes that implement this interface must provide concrete implementations of all its methods and properties.

class Elephant {
  // Public property, part of the public interface
  final String name;

  // Private property, only visible within this library (file)
  final int _id = 23;

  // Constructor is not part of the interface
  Elephant(this.name);

  // Public method, part of the public interface
  sayHi() => 'My name is $name.';

  // Private method, not accessible outside this library
  _saySecret() => 'My ID is $_id.';
}

In the Elephant class:

  • name is a public property and part of the class's public interface.
  • _id is a private property indicated by the underscore (_) prefix. It is only accessible within the file where Elephant is defined.
  • The constructor Elephant this.name initializes instances but is not considered part of the interface.
  • sayHi() is a public method and part of the public interface.
  • _saySecret() is a private method due to the underscore prefix, making it inaccessible outside of its defining library.

The role of Interfaces

In Dart, interfaces are used to define a contract that other classes can implement. This is crucial in a programming paradigm that encourages decoupling and polymorphism, where classes can interact with each other through well-defined interfaces rather than concrete implementations.

Prefixing a variable or method with an underscore _ is Dart's way of defining private members. Private members are accessible only within the file in which they are declared, providing encapsulation and helping to prevent unintended interactions with the class internals.

Superclasses and Subclasses

In Dart, the concept of superclasses (parent classes) and subclasses (child classes) is fundamental to object-oriented programming. These concepts are used to create a hierarchy of classes, allowing for code reuse and better organization.

Superclass or Parent Class

A superclass, also known as a parent class, provides common behaviors or properties that can be shared by multiple subclasses. Using the abstract keyword indicates that a class is intended for inheritance and not for creating instances directly.

abstract class Dog {
  void walk() {
    print('walking...');
  }
}

In this example, Dog is an abstract superclass. It has a method walk, which provides a general behavior that can be used or modified by its subclasses. Being abstract, Dog is not meant to be instantiated on its own.

Subclass or Child Class

A subclass, or child class, inherits properties and methods from the superclass. It can also have additional properties and methods or override inherited ones.

class Pug extends Dog {
  String breed = 'pug';

  @override
  void walk() {
    super.walk(); // Calling the superclass method
    print('I am tired. Stopping now.');
  }
}

The use of superclasses and subclasses allows for hierarchical class structures in Dart. This is possible with some code syntax here :

  • extends Dog indicates that Pug is a subclass of Dog.
  • String breed = 'pug' is a property specific to the Pug subclass.
  • The walk method is overridden to provide specific behavior for a Pug. The @override annotation indicates that this method is intentionally overriding a method from the superclass.
  • super.walk() within the overridden walk method calls the original walk method from Dog, demonstrating how a subclass can incorporate behavior from its superclass.

In summary, superclasses and subclasses in Dart facilitate the creation of organized, reusable code and we will use it all the time in flutter 😎

Generics

Generics are a way to parameterize types. They allow a class to wrap a type, and then use that type in multiple places. For example, we can have a Box class that wraps an double or String type.

Box<String> box1 = Box('cool');
Box<double> box2 = Box(2.23);

A generic type is a type that can be used as a substitute for a type parameter.

class Box<T> {
  T value;
  Box(this.value);
  T openBox() {
    return value;
  }
}

Packages

When you import libraries that might have conflicting names, you can create a namespace to differentiate them.

import 'somewhere.dart' as External;

In this example, somewhere.dart is imported with a namespace External. This means you can refer to any class or function from somewhere.dart by prefixing it with External., which helps to avoid name conflicts with other libraries.

Futures

In Dart and Flutter, Futures are a fundamental part of handling asynchronous operations. They allow your program to start a task that will complete in the future while moving on to other tasks.

Creating a Future

Futures are used to represent a potential value, or error, that will be available at some time in the future. They're often returned by APIs to handle asynchronous operations.

// futures.dart
var delay = Future.delayed(Duration(seconds: 5));

Handling a Future

Futures can resolve to a value (success) or an error. The then and catchError methods are used to handle these outcomes.

delay
    .then((value) => print('I have been waiting'))
    .catchError((err) => print(err));

Here, then is called if the future resolves successfully, and catchError is used to handle any errors that occur. This pattern ensures that you handle both successful and unsuccessful asynchronous operations.

Async-Await

The async and await keywords provide a more readable syntax for writing asynchronous code. async is used to mark a function as asynchronous, indicating that it returns a Future. await is used to pause the function's execution until a Future resolves.

Future<String> runInTheFuture() async {
  var data = await Future.value('world'); // Waits for the Future to resolve

  return 'hello $data';
}

In the runInTheFuture function, await pauses the function until Future.value('world') resolves. This results in a more linear and readable style compared to chaining then and catchError methods.

Futures are essential in Dart, especially for Flutter development for non-blocking code (allow the program to perform other tasks while waiting for an asynchronous operation to complete), error handling and cleaner code.