Let's talk (About) Dart, the language of Flutter

Jan 1, 2024

This article is part of the series:

In the world of mobile application development, Flutter has stood out as one of the most innovative and efficient frameworks. However, the real hero behind its success is the programming language that powers it: Dart. Previously, we have seen the basic concepts behind Flutter Widgets and a bit of their history, now we will dive into its details, exploring its origins, features, and how it is used in the context of Flutter to create mobile, web applications, and soon video games.

Origins

Dart, a modern and scalable programming language, was developed by Google and introduced to the world in 2011. It emerged as an alternative to JavaScript, with the goal of addressing some of the limitations that developers faced when using it, especially in terms of structure and performance. Dart was designed to be both familiar and easy to learn for programmers with experience in other object-oriented languages like C++ or Java, offering a clear and concise syntax.

Since its conception, Dart was envisioned as a versatile language, suitable for development both on the client and on the server side. Its nature, being compiled to machine code, makes it especially powerful for mobile applications, where performance and fluidity are critical. In addition, Dart was optimized to work efficiently on the web, allowing developers to compile high-performance applications that can run in any modern browser.

Features like Just-In-Time (JIT) compilation for rapid development and Ahead-Of-Time (AOT) compilation for optimal performance at runtime, determined the choice of Dart as the ideal option for the framework. Dart facilitates the creation of high-quality applications with smooth animations and a responsive user interface, key elements in the mobile user experience.

Basic Concepts

We are talking about an object-oriented programming language, with syntax similar to Java or C++. In Dart, as in many other programming languages, variables are a basic component. You can declare variables of different data types, including: int, double, String, bool, List, Map, Set, and many others.


    int age = 25;
    double height = 1.75;
    String name = 'Dart';
    bool isDeveloper = true;
    List<String> hobbies = ['programming', 'reading', 'running'];
    Map<String, String> languages = {'es': 'Spanish', 'en': 'English'};
    Set<String> skills = {'Dart', 'Flutter', 'Java', 'C++'};
    

To execute Dart code, we don’t need a complete IDE, although there are plugins and tools for the most common ones, it can be used as a scripting language and run directly from the command line by having it installed on our system. We can even run its source code directly from the web browser first if we just want to try it out.

Data Typing, Constants, and Variables

It is statically typed, which means that the type of a variable is defined at the moment of its declaration and cannot change. This helps detect errors at compile time and improves code performance. However, Dart also offers some flexibility with dynamic typing when necessary.

  • Static Typing: You explicitly declare the type of each variable.
  • Type Inference: It can infer the type of a variable based on the assigned value.
  • Dynamic Typing: You can use the special dynamic type if you need a variable that can change type.

    dynamic dynamicVariable = 'Hello Dart';
    
    void main(){
      dynamicVariable = 100; // Allows changing the type
      print(dynamicVariable); // Output: 100
    }
    

Even as a statically typed language, the compiler is capable of inferring the type of a variable based on the value assigned to it at the time of its declaration, allowing the use of var. This means that even if you do not explicitly specify the type of a variable when using var, the compiler determines and fixes the type of that variable based on the initial value it receives.


    var name = 'Dart'; // Dart infiere que 'name' es de tipo String
    name = 123; // Esto resultaría en un error porque 'name' es un String
    

Nevertheless, it is advisable not to overuse var and to specify the type of variables whenever possible as this facilitates code readability and helps in error detection. In their documentation, you can find more information about the typing system in Dart.

The use of constants is a common practice in programming, and Dart is no exception. These, like in any other language, cannot change during the execution of the program. You can declare constants using the const or final keyword. final is usually used when you need a variable that is only assigned once and can be calculated at runtime. On the other hand, use const for values that never change and can be determined at compile time, ensuring that all instances of that value in the program are identical and immutable.


    const String name = 'Dart';
    final DateTime now = DateTime.now();
        

Functions

Functions in Dart are blocks of code that perform a specific task and can be reused in different parts of your program. You can define functions with or without parameters and specify the return type. If you do not specify the return type, the default return type is dynamic.


    // Function without parameters and no return
    void sayHello() {
      print('Hello Dart!');
    }
    
    // Function with parameters and no return
    void sayHelloTo(String name) {
      print('Hello $name!');
    }
    
    // Function with parameters and a return value
    String sayHelloTo(String name) {
      return 'Hello $name!';
    }
    

Dart offers the possibility to define anonymous functions, which are nameless functions that can be useful in various situations, such as being assigned to variables or passed as parameters to other functions. They are a powerful and flexible resource, commonly used in situations where brief or single-use functions are required, such as in callbacks.



    // Anonymous function assigned to a variable
    var sayHello = () {
      print('Hello Dart!');
    };
    
    // Anonymous function passed as a parameter
    void sayHelloTo(String name, Function callback) {
      callback(name);
    }
    
    // Calling the 'sayHelloTo' function, passing an anonymous function as a parameter
    sayHelloTo('Dart', (name) {
      print('Hello $name!');
    });
    
    // Anonymous function with parameters and return value
    var numbersList = [1, 2, 3, 4, 5];
    var squares = numbersList.map((number) {
        return number * number;
    });
    
    // Printing the squares of the numbers
    print("Squares of the numbers: $squares");
    

Using return or a callback changes the way values are returned from a function. When using return, you are returning a value directly from a function to the point where that function was called. This is useful when you want to obtain an immediate and synchronous result from a function.


    int sum(int a, int b) {
      return a + b;
    }
    
    void main() {
      var result = sum(5, 3);
      print("The sum is: $result");
    }
    

A callback mechanism involves passing a function as an argument to another function so that it can be executed at a later time or in response to a specific event. This is especially useful in asynchronous situations or when you want to perform certain actions after an operation is completed. Here is an example:


    void performOperation(int a, int b, void Function(int) callback) {
      int result = a + b;
      // Simulates an asynchronous operation, like a network request or a timer.
      Future.delayed(Duration(seconds: 2), () {
        callback(result);
      });
    }
    
    void main() {
      performOperation(5, 3, (result) {
        print("The sum is: $result");
      });
    }
    

In this case, the response would arrive two seconds after executing the function. As we can observe, the difference in obtaining the result from each function is that with return, we get the value of the function immediately, whereas with a callback, we get the value of the function at a specific moment. In this way, we can perform asynchronous operations and obtain the result when it is available, but this is not the only method. In Dart, we can also use async and await, as well as Future, to perform asynchronous operations.

Asynchrony

There are several mechanisms in Dart for performing asynchronous operations, the most used ones besides async/await are Future. A Future is an object that represents a potential value or an error that will be available at some point in the future. They are used to perform asynchronous operations, such as network requests or database queries, and to obtain the result when it becomes available.


    // Defines an asynchronous function that takes two numbers and returns a result after 2 seconds.
    Future<int> performOperation(int a, int b) async {
      int result = a + b;
      // Simulates an asynchronous operation, like a network request or a timer.
      await Future.delayed(Duration(seconds: 2));
      return result;
    }
    
    void main() async {
      print("Beginning of the program");
    
      // Calls performOperation and waits for it to complete.
      int result = await performOperation(5, 3);
    
      print("The result is: $result");
      print("End of the program");
    }
    

It’s common for mobile applications to need to perform asynchronous operations, such as network requests or database queries. In these cases, it’s advisable to use async and await to carry out these operations asynchronously. We can also use Future to request data without knowing in advance how long it will take to become available.

The combination of each of the mentioned techniques is ultimately up to the developer and depends on the needs and context of each application. In the Dart documentation, you can find more information on asynchrony which I recommend you visit to delve deeper into the topic.

Control Structures

Dart accepts all types of control structures like any other programming language, including if, else, switch, for, while, etc. As we are in a series of articles on Flutter, we will assume that the basic control structures of any programming language are already known. We will mention some of the most used and specific to Dart, even though they may also exist in other languages.

Ternary Operator

The ternary operator is a shortened form of the if-else structure and is used to assign a value to a variable or to perform an operation based on a condition.

    
    // Assigns the value 'Dart' to the variable 'name' if 'isDart' is true, otherwise assigns 'Flutter'.
    String name = isDart ? 'Dart' : 'Flutter';
    

Cascade Operator

The cascade operator (..) is a shorthand way of calling multiple methods on an object. This is especially useful when you need to perform several operations at once, such as adding multiple items to a list or setting several properties of a widget.


    // Creates a list and adds several elements at once.
    var numbersList = []
      ..add(1)
      ..add(2)
      ..add(3)
      ..add(4)
      ..add(5);
    
    // Creates a widget and sets several properties at once.
    var widget = Container()
      ..color = Colors.blue
      ..width = 100
      ..height = 100;
    

Spread Operator

The spread operator is a shorthand way of adding all the elements of one list to another list. This way, it’s not necessary to iterate through the list to add each element to the new list.


    var numbersList = [1, 2, 3, 4, 5];
    var newNumbersList = [0, ...numbersList];
    

Null-aware Operator

The null-aware operator is a shorthand way of checking if a variable is null and assigning a default value in case it is.


    String? predefinedName = 'Alice';
    // Assigns the value 'Alice' to the variable 'name' if 'predefinedName' is not null, as in this case, otherwise assigns 'Bob'.
    String name = predefinedName ?? 'Bob';
    

Null-aware Conditional Access Operator (?.)

The null-aware conditional access operator is a shorthand way of accessing a property of an object only if that object is not null. If it is null, the property is not accessed.


    User? user = User();
    // Accesses the 'name' property of the 'user' object if 'user' is not null.
    String name = user?.name ?? 'Dart';
    

Classes and Objects

With classes and objects, it’s more of the same; Dart is an object-oriented programming language and therefore has them like any other language of this type. To create a class in Dart, the keyword class is used, followed by the name of the class and its definition. Let’s look at an example of a class called Person that has basic attributes and methods:


    class Person {
      String name;
      int age;
    
      Person(this.name, this.age);
    
      void introduceYourself() {
        print('Hello, my name is $name and I am $age years old.');
      }
    }
    
    void main() {
      var person = Person('Alice', 30);
      person.introduceYourself(); // Output: Hello, my name is Alice and I am 30 years old.
    }
    

You can receive optional parameters by using curly braces {} in the constructor definition to create a more flexible constructor that allows sending the values defined in it on demand.


    class Person {
      String name;
      int age;
      String? hairColor; // Optional parameter
    
      Person(this.name, this.age, {this.hairColor});
    
      void introduceYourself() {
        if (hairColor != null) {
          print('Hello, my name is $name, I am $age years old and my hair is $hairColor.');
        } else {
          print('Hello, my name is $name and I am $age years old.');
        }
      }
    }
    
    void main() {
      var person1 = Person('Bob', 30);
      var person2 = Person('Alice', 25, hairColor: 'Blue');
    
      person1.introduceYourself(); // Output: Hello, my name is Bob and I am 30 years old.
      person2.introduceYourself(); // Output: Hello, my name is Alice, I am 25 years old and my hair is Blue.
    }
    

Null Safety

The evolution of this language has been significant since its creation and has incorporated new features and improvements over time. One of the most important is Null Safety, which was introduced in Dart version 2.12 and is one of the main reasons why Flutter decided to migrate to Dart version 2.12. It allows developers to avoid runtime errors related to the null value.


    String? name = null;
    print(name.length); // Error: The 'length' property can't be accessed unconditionally because the receiver could be 'null'.
    

In the previous example, the variable name is of type String?, which means it can be either String or null. If the variable name is null, the program will fail at runtime because the length property cannot be accessed from a null value. To avoid this error, we can use the null-aware conditional access operator ?. to access the length property only if the variable name is not null.


    String? name = null;
    print(name?.length); // No error
    

In the Dart documentation, you can find more information about Null Safety which I recommend you visit to delve deeper into the topic.

Conclusion and Example

We have seen the basics of Dart, the types of data that exist, the functions, and the fundamentals to understand the language. I invite you to put into practice what you’ve learned in this article with a small exercise by creating a simple application to manage tasks via the command line that allows you to create, list, and delete tasks. Once finished, you can compare it with this example of what the final source code could be, where you can identify many of the concepts explained here.



    import 'dart:io';
    
    class Task {
      String description;
      bool isCompleted;
    
      Task(this.description, this.isCompleted);
    
      // Method to convert a task into a string that can be saved in a file.
      String toFileString() {
        return '$description|$isCompleted';
      }
    
      // Static method to create a task from a string retrieved from a file.
      static Task fromFileString(String line) {
        final parts = line.split('|');
        final description = parts[0];
        final isCompleted = parts[1] == 'true';
        return Task(description, isCompleted);
      }
    }
    
    class TaskList {
      List<Task> tasks = [];
    
      void addTask(String description) {
        final task = Task(description, false);
        tasks.add(task);
      }
    
      void completeTask(int index) {
        if (index >= 0 && index < tasks.length) {
          tasks[index].isCompleted = true;
        }
      }
    
      void removeTask(int index) {
        if (index >= 0 && index < tasks.length) {
          tasks.removeAt(index);
        }
      }
    
      void displayTasks() {
        print("Tasks:");
        for (var i = 0; i < tasks.length; i++) {
          final task = tasks[i];
          final status = task.isCompleted ? "Completed" : "Pending";
          print("$i. ${task.description} ($status)");
        }
      }
    
      // Method to save the task list to a file.
      void saveToFile(String filename) {
        final file = File(filename);
        final lines = tasks.map((task) => task.toFileString()).toList();
        file.writeAsStringSync(lines.join('\n'));
      }
    
      // Method to load the task list from a file.
      void loadFromFile(String filename) {
        final file = File(filename);
        if (file.existsSync()) {
          final lines = file.readAsLinesSync();
          tasks = lines.map((line) => Task.fromFileString(line)).toList();
        }
      }
    }
    
    void main() {
      final taskList = TaskList();
      final filename = 'tasks.txt';
    
      // Load tasks from the file (if it exists).
      taskList.loadFromFile(filename);
    
      while (true) {
        print("Choose an action:");
        print("1. Add task");
        print("2. Show tasks");
        print("3. Complete task");
        print("4. Remove task");
        print("5. Save and Exit");
    
        var choice = int.tryParse(stdin.readLineSync() ?? '');
    
        switch (choice) {
          case 1:
            print("Enter the task description:");
            var description = stdin.readLineSync() ?? '';
            taskList.addTask(description);
            break;
          case 2:
            taskList.displayTasks();
            break;
          case 3:
            taskList.displayTasks();
            print("Enter the task number to mark as `completed`:");
            var index = int.tryParse(stdin.readLineSync() ?? '');
            taskList.completeTask(index ?? -1);
            break;
          case 4:
            taskList.displayTasks();
            print("Enter the task number to remove:");
            var index = int.tryParse(stdin.readLineSync() ?? '');
            taskList.removeTask(index ?? -1);
            break;
          case 5:
            // Save tasks to the file and exit.
            taskList.saveToFile(filename);
            exit(0);
            break;
          default:
            print("Invalid choice. Please try again.");
        }
      }
    }
    

In this article, we have explored the basics of Dart, the programming language that powers application development in Flutter. We’ve covered essential concepts, such as data types, functions, classes, and objects, as well as more advanced features, including asynchronous programming.

We demonstrated how to create interactive programs in Dart, such as a task list with the ability to add, display, complete, and delete tasks, and how to persist data in files to make tasks accessible over time. For the next one, we will continue delving into Flutter and start looking at some examples.

Happy Coding!

Related posts

That may interest you

September 15, 2023

Flutter introduction

Flutter is a mobile app development framework created by Google. It utilizes the Skia rendering …

read more