From time to time, every person who deals with coding might need a reminder of some concepts not used very often. In this article, I tried to cover as many essential Dart language keywords as I could think of. However, if you don't find what you are looking for, please leave a comment - I'll update this post if necessary. Have a good reading! 🙂
Basic Types & Variable Declaration
Let's begin our dictionary with, I believe, the simplest section and, at the same time, the fundamental one. It contains keywords used for declaring variables in Dart and an explanation of the basic Dart types.
String - used for declaring variables representing text.
String text = 'Hello';
num - used for declaring any kind of number (parent type for int and double)
int - used for declaring integer numbers (without fractional part)
int number = 3;
// this won't work:
int number = 2.3;
double - used for declaring fractional numbers
// Both below are OK
double firstNumber = 5;
double secondNumber = 4.2;
bool - used for declaring boolean values (true/false)
bool isDone = true;
bool isNotDone = false;
List - used for declaring lists. You can specify the type of list's content by adding <type> after the keyword.
List<String> exampleList = ['Dog', 'Cat', 'Rabbit'];
Set - is an iterable, just like List, but here values can't be repeated
void main() {
Set<String> exampleSet = {'Tom', 'Bob', 'Bob', 'Mark', 'Tom'};
print(exampleSet); // output is: {'Tom', 'Bob', 'Mark'}
}
Map - maps are collections of key - value pairs. You can additionally specify variable type of keys and values
void main(){
Map<String, String> data = {'name': 'Jan', 'lastName': 'Kowalski'};
print(data); // output is: {name: Jan, surname: Kowalski}
}
var - used to declare a variable without specifying its type. However, the variable's type is declared by its first assignment.
var variable = 2;
// Changing variable's type won't work. Below code produces an error:
variable = 'Hello';
dynamic - used to declare variables without specifying their initial type. Dynamic variables can change type after declaration.
dynamic dynamicVariable = 10.102;
// Changing dynamicVariable's type is fine.
// Below code doesn't produce an error:
dynamicVariable = 'Text';
Function - it is a base class for all function variables. As Dart is an object-oriented programming language, you may assign functions to variables or pass them as an argument to other functions.
class Person {
static Function sayHi = (String name) => print('Hi, my name is $name');
}
void main (){
Person.sayHi('Emma');
}
The output of the code above is:
Hi, my name is Emma
null - every variable in Dart needs to have a type. You can declare variables with ? sign, which means they can be either the type you declared or null ex:
String? name;
Since Dart's null safety was introduced, you should use this feature. It will throw static errors for not initialized non-nullable variables. Null safety prevents the app from crashing unexpectedly.
final - a final variable is single assignment only. Its value can't change.
final int number = 10; // OK
final int databaseNumber = getNumberFromDB(); // OK
// Below code won't work, as number was already assigned value
number = 5;
const - a value of constant variable is not only single assignment, but also must be known at compile time.
const name = 'Laura'; // OK
// Below code produces an error, as
// value downloaded from database is not known at compile time
const databaseName = getNameFromDB();
// Below code produces an error, as
// const variable can't change its value
name = 'Caroline';
late - used for lazy declaration of variables and declaring a non-nullable variable, which is initialized after its declaration. Useful when a variable is never used and initializing it requires a lot of resources. You should be careful about using late with uninitialized, non-nullable variables. If you forget or fail to initialize such a variable, you will encounter a runtime error.
// #1
late int age = getUserAge();
// if age variable is not used - then an expensive function
// getUserAge() doesn't have to run.
// #2
late String hi;
void main () {
hi = 'Hello';
print(hi); // here You would encounter an error,
// if late variable wasn't initialized.
}
required - used to denote named parameters, which should be mandatory to declare for an object. For better understanding, let's establish a distinction between positional and named parameters:
positional arguments are required by default - You can mark them as optional.
{named arguments} are optional by default - You can mark them as required.
Below I created an example of Month class. It has three fields: positional parameter (required by default) days, required named parameter name, and one optional named parameter season. As we can see - it is possible to create an instance of a month without a season parameter; however, we can't create one without days or name.
class Month {
int days;
String name;
String? season;
Month(this.days, {required this.name, this.season});
}
void main () {
Month may = Month(31, name: 'May');
Month june = Month(30, name: 'June', season: 'Summer');
}
Interface
Experienced developers may be familiar with the concept of the interface from other programming languages, but it may be a bit challenging for beginners to grasp the idea. You may think of the interface as a way a project is organized - how classes interact with each other and influence one another.
For example, a class representing a person will have a certain relation with another class representing an employee, and your common sense should help you feel it. What's left is to translate this feeling into code :)
class - if you are familiar with object-oriented programming concepts, then class is like a blueprint for an object. Inside a class, you can specify some characteristics, such as fields (variables), constructors, methods, etc.
interface - it is an object-oriented programming concept, a set of rules a class must refer to. In Dart, there is no syntax dedicated to declaring interfaces. The interface is set by the way classes are declared.
abstract - used for declaring abstract classes. An abstract class can't be instantiated. They are typically used for the interface. Abstract classes are often extended by other classes.
implements - used for implementing an interface in a class. Once a class implements another class, it has to override all methods and variables of the implemented one.
import - we use this keyword when we want access to any external libraries or other files from the project in our current file.
static - used before variables/methods we would like to access without instantiating a class.
new - can be used when declaring a new instance of a class, but doesn't have to - optional keyword.
abstract class Animal {
void walk();
}
class Dog implements Animal {
@override
void walk(){
print('Dog walking');
}
static int age = 6;
}
void main(){
// Animal animal = Animal();
// produces an error as abstract classes can't be instantiated.
// Both declarations below are correct:
Dog puppy1 = Dog();
Dog puppy2 = new Dog();
puppy2.walk();
print(Dog.age);
// Dog.walk();
// produces an error as instance members can't be accessed by static access.
// print(puppy.age);
// produces an error as static members can't be accessed through an instance.
}
and the output of the code above is:
Dog walking
6
part/part of - used for setting a better organization pattern of a library, splitting data into separate files. Using part, we have access to private variables of the main file, contrary to using import, when we have access only to its public members.
extends - used to create a more specific version (subclass) of an existing class. If class A extends class B (class A is a subclass of class B), all properties and methods of class B are also available in class A. We can also override methods. We can use another keyword - super to refer to the parent class. A class may extend only one class at a time.
super - used to refer to the parent class methods/properties.
class Car {
String name = 'car';
}
class CarEngine extends Car {
String engineOn = 'engine turned on!';
void startTheCar () {
print('start the ${super.name}...');
print(engineOn);
}
}
void main () {
CarEngine engine = CarEngine();
engine.startTheCar();
}
prints:
start the car...
engine turned on!
this - when declaring a new object of some type, you may run into the name clash:
class Book {
int? length;
Book(int length) {
length = length;
}
}
As you can see, we have a name conflict. The instance 'length' has the same name as the class property, and Dart assigns a value to itself. That's the reason this keyword was introduced - to describe properties of the current instance:
class Book {
int? length;
Book(int length) {
this.length = length;
}
}
Side note: it is recommended to use initializing formals when possible. For further info, please visit this guide
mixin (on) - used to reuse code in multiple (possibly different level) class hierarchies. It allows the use of other class code without being its child. You may define mixin just like a normal class or using mixin keyword. By adding “on”, you can restrict mixins to be used only with certain classes.
with - used on mixins, similar application to extends or implements.
extension - used to add functionality to already existing libraries like classes. For example, you may imagine a situation in which, for whatever reason, you'd like to create an extension that multiplies variable of type int by two. For this purpose, we can simply write:
extension IntTimesTwo on int {
int timesTwo() => this * 2;
}
void main () {
int number = 4;
print(number.timesTwo());
}
and in the console, we see:
8
factory - used for creating constructors that don't always return a new instance of a class. The typical application is to generate objects from json data.
class User {
User({this.name, this.lastName, this.email, this.password,});
final String? name;
final String? lastName;
final String? email;
final String? password;
factory User.fromJson(Map<String, dynamic> json) =>
User(
name: json['name'] as String?,
lastName: json['lastName'] as String?,
email: json['email'] as String?,
password: json['password'] as String?,
);
}
void main () {
Map<String, dynamic> jsonData = {
'name': 'Tom',
'lastName': 'Black',
'email': 'tom.black@email.com',
'password': 'asqed********',
};
User userTom = User.fromJson(jsonData);
print(userTom.name);
print(userTom.lastName);
print(userTom.email);
print(userTom.password);
}
after running the code above, we confirm that the factory constructor works by reading from the console:
Tom
Black
tom.black@email.com
asqed********
operator - used to override default Dart operators inside a class. For example, hypothetically, you may want to check if one month is longer than the other without accessing its properties directly. In that case, let's override > operator for class Month:
class Month {
int days;
String name;
Month(this.days,this.name);
operator >(Month m) {
if (days > m.days) {
return true;
} else {
return false;
}
}
}
void main () {
Month may = Month(31,'May');
Month june = Month(30,'June');
print(may>june);
}
as May is a longer month than June, this code outputs:
true
covariant - its typical application is to disable static error when overriding a parameters type with its subtype. In the example below, without a covariant keyword, we would get an invalid function type override error (due to different object types).
abstract class Human {
void welcome(Human h ) {}
}
class Customer extends Human {
String? name;
Customer(this.name);
}
class ShopAssistant extends Human {
@override
void welcome(covariant Customer c) {
print('Welcome ${c.name}, how can I help you?');
}
}
void main () {
ShopAssistant shopAs = ShopAssistant();
Customer customer = Customer('Robert');
shopAs.welcome(customer);
}
the output of the code above is as expected:
Welcome Robert, how can I help you?
enum - it's a class designed to define a fixed number of constant values. It helps avoid typing errors when working with the same values in different places of the app.
enum Shapes { rectangle, circle, triangle }
enum Fruits { apple, orange, grapes, pear, cherry }
get/set - so called getters and setters are methods that provide read/write access to the properties of an object, outside of the class.
class Car {
double passengersWeight;
double carWeight;
Car({required this.passengersWeight, required this.carWeight});
double get totalWeight => passengersWeight + carWeight;
}
void main () {
Car vehicle = Car(passengersWeight: 350, carWeight: 2400);
print(vehicle.totalWeight);
}
prints:
2750
show/hide - used for selective imports. Use these keywords if you want to import just a part of the library to your file
// Import only function1.
import 'package:lib/file1.dart' show function1;
// Don't import function2, import everything else.
import 'package:lib/file2.dart' hide function2;
Control Flow Statements
Control flow statements are the core functionality of Dart and many more programming languages. They execute conditional code procedures with looping that allows iterating through the code. Every Dart/Flutter trainee gets familiar with them at the beginning of their journey.
if - together with while, for, and switch, they are the basic conditional expressions in Dart. Using an if and optional else statements lets you control the flow of the app. “If” can be used for multiple purposes. You can use it inside a function, generate UI based on state, or navigate different routes depending on some condition.
else/else if - typically used together with if statement, placed after the first if block to specify instructions when the first constraint isn't satisfied.
for - control flow statement used for looping through the code until some condition is satisfied. In the for loop declaration, we need to specify three things (order of declaration matters): first - the variable on which the loop depends; second - the condition until which the loop proceeds; and the third is an operation on the variable after each iteration.
void main () {
for(int number = 7; number <= 23; number++ ){
if (number <= 10) {
print('Number $number is smaller than, or equal to 10');
} else if (number > 10 && number < 20) {
print('Number $number is between 10 and 20');
} else {
print('Number $number is bigger than, or equal to 20');
}
}
}
and the output of the code above is:
Number 7 is smaller than, or equal to 10
Number 8 is smaller than, or equal to 10
Number 9 is smaller than, or equal to 10
Number 10 is smaller than, or equal to 10
Number 11 is between 10 and 20
Number 12 is between 10 and 20
Number 13 is between 10 and 20
Number 14 is between 10 and 20
Number 15 is between 10 and 20
Number 16 is between 10 and 20
Number 17 is between 10 and 20
Number 18 is between 10 and 20
Number 19 is between 10 and 20
Number 20 is bigger than, or equal to 20
Number 21 is bigger than, or equal to 20
Number 22 is bigger than, or equal to 20
Number 23 is bigger than, or equal to 20
do - used together with while works the same way, just the syntax is a little bit different - first, you declare the code that has to be executed and only afterward the condition.
Below are shown two code snippets, the first one is while, and the second is a do / while loop - both with exactly the same output:
10
11
12
while:
void main () {
int number = 10;
while (number <=12) {
print(number);
number++;
}
}
do/while:
void main () {
int number = 10;
do {
print(number);
number++;
} while(number <= 12);
}
switch - used in a similar way as an if statement; however, switch has a little leaner syntax. It's typically applied in places with one simple condition.
case - used inside the switch statement. As the name suggests it separates cases. Works similar to else if in the if statement.
default - used to handle a default (unimplemented) case in a switch statement. If you are not 100% sure that the cases in the switch statement cover all possible scenarios, you should use the default case to ensure error safety.
break - keyword telling Dart to exit the loop. As in the example below - we tell Dart to exit the loop (end the execution of a function) when one of the conditions is met. In our case, as num was equal to 5. But we had to go through the whole function anyway.
continue - used to terminate the current iteration of a function. You can use it to reverse logic in comparison to the most common pattern. Typically we tell our program to carry out some operation if a condition is met. Using continue, you may tell Dart to end iteration if a condition is met.
Below an example of switch statement with other keywords:
void main () {
int num = 5;
switch(num){
case 1:
print('num is smaller than 2');
break;
case 2:
print('num is equal to 2');
break;
case 3:
print('num is bigger than 2');
break;
default:
print('num is out of collection {1,2,3}');
}
}
and the output is:
num is out of collection {1,2,3}
Asynchronous Programming & Error Handling
Let's now focus on keywords you may need while working with code that needs to run asynchronously.
async - placed before a body of the function allows for asynchronous operations. Usually used together with the await keyword. async functions usually return Future object.
await - used as a signal for the asynchronous function to wait for the result of the operation, which is directly after this keyword.
return - indicates the place in code where an async function should end. After return, you can also specify what should be the output of the function. Return value needs to match Future<value> type of the function.
String? name;
int? age;
Future<void> getDatabaseResult () async {
var data = await DB.getData(); // We need to wait for response from
name = data.name; // the database in order to proceed
age = data.age; // with execution of this function.
return;
}
async* - similar to the async keyword, the difference is that the same way as async functions return Future, async* function returns Stream - a sequence of events.
yield/yield* - it is a stream equivalent of Future's return. yield is used to add result (an event) to the stream which is an output of the async* function, and yield* can be used to insert a series of elements, as there was yield for each one of them.
try - try/catch workflow was designed in order to allow for execution of code without having to care about exceptions. First, the method runs the try block and if it encounters an error - it jumps to the catch block.
catch - when there is an error in the try block, catch is the place for the developer to handle such an exception.
on - can be used to handle a specific type of error.
finally - part of the function, which runs in both cases - if there was an error and if there wasn't one.
Stream<UserResponse> getUserName async* {
try {
final user = await someUserDB.getUser();
final String userName = user.name;
yield UserResponse(text:'User name: $userName');
} on SomeConnectionError {
print('No connection');
yield UserResponse (text:'No connection');
} on SomeEmptyNameError {
print('User name missing in the database');
yield UserResponse (text:'User name missing in the database');
} catch (e) {
print(e);
yield UserResponse(text:'Unable to get name from database');
} finally {
print('End of function');
}
}
throw - used to throw an exception (can be a custom class)
throw WrongNameException('Name must be at least 3 characters long');
rethrow - allow you to pass caught exception to higher level, for example when you have try/catch block nested in another try/catch block
void check() {
try {
dynamic variable = true;
print(variable * 2);
} catch (e) {
print('error ${e.runtimeType} in check()');
rethrow; // Pass error up a level
}
}
void main() {
try {
check();
} catch (e) {
print('error ${e.runtimeType} in main()');
}
}
output of the code above is:
error JsNoSuchMethodError in check()
error JsNoSuchMethodError in main()
assert - used in a pattern assert(condition, message). You can add asserts to stop execution of a program, when the condition specified returns false - then display a message in the console.
double calculateCostPerPerson(double totalCost, int numberOfPeople) {
assert(totalCost > 0, 'cost has to be higher than 0');
assert(numberOfPeople != 1, 'no point in dividing cost for just one person');
assert(numberOfPeople > 1, 'number of people must be higher than 1');
return totalCost/numberOfPeople;
}
void main() {
calculateCostPerPerson(1000,1);
}
this code outputs an error:
Uncaught Error: Assertion failed: "no point in dividing cost for just one person"
Type Operators
In the end, we are left with keywords commonly used for casting/checking an object's type in Dart and declaring new types.
as - together with is, it is used to check objects' types at runtime. Use this keyword for casting only these objects, which types you are sure of.
is - contrary to as it is used for checking the type of an object you are unsure of.
// example for as, if robert is not Human, throws an Exception
(robert as Human).age = 20;
// example for is, if robert is not Human, does nothing.
if(robert is Human) {
robert.age = 20;
}
typedef - keyword used to declare new types as a combination of already existing ones. You can later use it together with is keyword to check if your variable is of the correct type.
typedef SetString = Set<String>;
void main () {
SetString set = {'one', 'two', 'one', 'two', 'three'};
if(set is SetString) {
print(set);
} else {
print('variable set is of different type than SetString');
}
}
prints:
{one, two, three}
To Sum Up
Thank you for reaching the end of this article. I hope you found it, or at least a part of it, helpful. If you wish to continue exploring the Dart language, I encourage you to take this tour. You will find much more knowledge there. To get familiar with the recommended approaches and good practices when working with Dart, it is worthwhile to read through this guide. And if you are only starting your programming journey - be patient and push on. You'll get there eventually :)
What You have to say about this - don't hesitate to comment :)