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
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
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
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.
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.
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 !
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.
Allowing Null values
To explicitly allow a variable to hold a null value, append a question mark ?
to its type.
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.
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.
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:
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.
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.
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:
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:
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.
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 thatPug
is a subclass ofDog
.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.
A generic type is a type that can be used as a substitute for a type parameter.
Packages
When you import libraries that might have conflicting names, you can create a namespace to differentiate them.
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.
Handling a Future
Futures can resolve to a value (success) or an error. The then and catchError
methods are used to handle these outcomes.
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.