Home/Blog/Learn to Code/Introducing Dart 2 language features
Home/Blog/Learn to Code/Introducing Dart 2 language features

Introducing Dart 2 language features

10 min read
Oct 23, 2020
content
How to use Dart 2 extensions for enhanced code flexibility
Extension methods
Extension operators
Extension property
Dart enums
Switch block with enums
Dart mixins
Mixins example
Dart generics
Asynchronicity in Dart
Future objects
Conclusion and next steps
Continue reading about mobile development and Flutter

Key takeaways

  • Dart 2 empowers developers with extensions, mixins, and generics, offering more flexible ways to extend libraries, share behaviors, and ensure type safety across reusable code components.

  • With enums for fixed states and robust handling of asynchronous tasks using Future and Stream classes combined with async and await, Dart 2 enables precise state control and efficient task management.

Dart is an object-oriented, class-based, simple, and clean language. Google’s popular mobile framework, Flutter, uses Dart to implement high-quality native applications. In 2018, Dart 2 was released with all sorts of new features that make it much safer, more expressive, and more usable.

This update modified the basics of the Dart language, libraries, build systems, and web development tools. Today, we’ll introduce you to the most important new language features of Dart 2 to take your web applications to the next level.

How to use Dart 2 extensions for enhanced code flexibility#

Dart extensions were officially released with Dart 2.7. The extensions feature adds functionality to existing Dart libraries. The extensions feature is useful when adding utility method(s) in a third-party library or a core class.

With the first version of Dart, adding methods in those classes was not always practical. Extensions help by implicitly extending the type, unlike the old style, where the object was explicitly passed as an argument to static methods.

Extensions are defined inside one block of code that begins with extension and contains a name for the extension, the on keyword, and the data type. Dart supports three types of extensions that you’ll look into in more detail below:

extension<T> on List<T> {
  //extension methods
  //extension operators
  //extension properties
}

Extension methods#

Extension methods allow you to add new members to existing types. You might already be using extension methods. For example, using code completion in an IDE will suggest an extension method. Extension methods are located in libraries.

Note: You use them by importing the right library and using them like an ordinary method.

Let’s see an example to understand extension methods. Below, we will write an extension method for the List data type. The extension method priceList() returns the price listing in the same condition. This method demonstrates how extensions implicitly extend the type using this.

//Local extension method
extension<T> on List<T> {
//Extension Method demonstration
List<T> priceList() => this.map((item) => item).toList();
}
void main() {
//List of prices
List prices = [1, 1.99, 4];
print("Price listing:");
//priceList() is being called on `prices` list
//and returns the list of prices
print(prices.priceList());
}

The output will be:

Price listing: [1,1.99,4]

Extension operators#

Dart also provides support for operators. Below, we will define an operator extension for the ^ operator. We assume this operator increases the price by n times, where n is the passed argument.

The operator keyword declares an extension operator, and the operator sign follows it. In the following example, this list is iterated to multiply each item by n, and then the updated list is returned.

extension<T> on List<T> {
  //Extension Operator: Hike up the price by n
  List<num> operator ^(int n) =>
    this.map((item) => num.parse("${item}") * n).toList(); 
}

The operator ^ is applied to the prices list. The operator ^ multiplies each item in prices by 3 and returns. The updated list is then printed using the print() method.

void main() {
  //List of prices
  List prices = [1, 1.99, 4];
 
  print("\nPrice listing after hiking up prices 3x of the original value");
 
  //argument is passed after the operator sign
  print(prices ^ 3);
}

This gives us the output below:

Price listing after hiking up prices 3x of the original value
[3, 5.97, 12]

Extension property#

Dart also provides support for properties. In the extension below, we add a property that returns the total number of printed price labels needed for a price listing. Let’s assume we want to print 3 labels for each price amount in the list [1, 1.99, 4].

An extension property definition has three parts: the type of data to be returned by the property, the get keyword, and the property name. For example:

<return_type> get <property_name> => //implementation

The number of labels is type int, and the property name is labelCount. We need 3 labels for each item, so we need three times the total size of the list. We can calculate the number of total labels as length * 3. The extension has implicit access to the length property.

extension<T> on List<T> {
  //Extension Property: 3 printed labels for each price.
  int get labelCount => length * 3;
}

The property labelCount is called on the price listing prices.

void main() {
  //List of prices
  List prices = [1, 1.99, 4];
 
  print("\nNumber of Printed Labels:");
  print(prices.labelCount);
}

This gives us the following output:

Number of Printed Labels:
9

Dart enums#

Enumerated types (or enums) were added with the release of Dart 1.8. Enums act like a class that represents a fixed number of constant values. For example, you could have an app fetching data from a remote server. The app shows one of the following statuses:

  • done: The app received the response successfully.
  • waiting: The app is waiting for the response.
  • error: The app received an error from the server.

The above responses can be declared using the enum keyword:

enum Status {
  done,
  waiting,
  error, //This comma is optional
}

Let’s understand this better with an example. Pretend we have a weather application. We need to represent the states of the weather: sunny, cloudy, and rainy.

We could represent these states with a const keyword.

const SUNNY = 'Sunny';
const CLOUDY = 'Cloudy';
const RAINY = 'Rainy';

Or, we could use enumerated types with the enum keyword.

enum Weather {
  sunny,
  cloudy,
  rainy,
}

Switch block with enums#

We can use the switch block for enums. It requires case blocks for all members of our enum class and a default block if a case-block implementation is missing; otherwise, you get a compilation error. Take a look at the example below:

Note: switch block implementations can be done for constants and enums. However, enums are preferred when you do not want to lose an opportunity to handle a particular case.

//Using Enums to display weather information
enum Weather {
sunny,
cloudy,
rainy,
}
void main() {
var weather = Weather.sunny;
//Following code will complain about if all members are not present
//Use default block when all case blocks are not available
switch (weather) {
case Weather.sunny:
print("Sunny weather today!");
break;
case Weather.cloudy:
print("Cloudy today!");
break;
case Weather.rainy:
print("Rainy and gloomy weather.");
break;
default:
print("Current weather:${weather}");
}
}

#

Keep the learning going!
Learn to develop web applications using Dart without scrubbing through videos or documentation. Educative’s text-based courses are easy to skim and feature live coding environments, making learning quick and efficient.

Developing Web Applications with Dart

Dart mixins#

Mixins allow our Dart code to be reusable across separate classes. Reusing code from classes that share common behaviors is far more efficient. A mixin class contains methods other classes use, but it is not their parent. Therefore, unlike interfaces and abstract classes, we can use the code from a class without inheriting it.

Mixins are declared using the mixin keyword:

mixin SharedBehavior {
 }

The syntax of mixins is simple. Below, B is the parent class to A. C is the mixin with methods that B can implement.

class A extends B with C {
    //Implement methods from B & C

}

Mixins example#

Let’s understand this better with an example. Say we have different people with different occupations: artists, engineers, doctors, and athletes. We can assume that the four types of people share common behaviors (like sketching, reading, exercise, boxing, etc.) in addition to inheriting the class Person.

In other words, each type of person extends the Person class and one or more shared behaviors.

Overlapping common behaviors like these can be extracted into mixins. We will create mixins for these behaviors. For example, the Sketching mixin defines the common sketch() method, which takes a message parameter.

mixin Sketching {
  sketch(String message) {
    print(message);
  }
}

Let’s see our example in code. Below, in the main() method, each class’s object is created, and method(s) are called for their shared behavior, which is implemented using mixins.

//Person class
abstract class Person {
int age;
int name;
eat() {}
sleep() {}
}
//Artist class
class Artist extends Person with Sketching {
sketchLandscape() {
sketch("Making landscapes sketches");
}
}
//Engineer class
class Engineer extends Person with Sketching, Reading {
sketchBuildings() {
sketch("Sketching engineering drawings");
}
readResearchPaper() {
String topic = "Building Construction";
dailyReading(topic);
}
}
//Doctor class
class Doctor extends Person with Reading, Exercise {
readReports() {
String topic = "flu";
dailyReading(topic);
}
workout() {
running(1);
weightTraining(10);
}
}
//Athlete class
class Athlete extends Person with Exercise {
generalRoutine() {
running(2);
weightTraining(20);
}
}
//Boxer class
class Boxer extends Athlete with Boxing {
punchPractice() {
punch(100);
}
routineExercise() {
running(4);
weightTraining(40);
}
}
//Mixins
//Sketching mixin
mixin Sketching {
sketch(String message) {
print(message);
}
}
//Reading mixin
mixin Reading {
dailyReading(String topic) {
print("Daily reading on ${topic}");
}
}
//Exercise mixin
mixin Exercise {
running(int mile) {
print("Daily run of ${mile} mile(s)");
}
weightTraining(int weights) {
print("Lifting ${weights} lbs");
}
}
//Boxing
mixin Boxing on Athlete {
punch(int n) {
print("Boxer practicing ${n} punches");
}
}
void main() {
print("Artist");
Artist artist = Artist();
artist.sketchLandscape();
print("\nEngineer");
Engineer engineer = Engineer();
engineer.sketchBuildings();
engineer.readResearchPaper();
print("\nDoctor");
Doctor doctor = Doctor();
doctor.readReports();
doctor.workout();
print("\nBoxer");
Boxer boxer = Boxer();
boxer.punchPractice();
boxer.routineExercise();
}

Dart generics#

Generics are used to apply stronger type checks at compile-time. They also enforce type safety. Generics help write reusable classes and methods/functions for different data types. Generics in Dart are similar to Java generics or C++ templates.

Definition: Type safety is a programming concept allowing a memory block to contain only one data type.

Dart’s collection can hold different data types in one collection, but the program can crash if a particular data type is not handled appropriately. Generics can solve this problem by enforcing one data type in the collection.

Let’s learn how to declare type safe collections. To ensure type safety, the angular brackets <> with data type enclosed declare the data type collection.

CollectionType <dataType> identifier = CollectionType <dataType>();

Generics are parameterized, using variable notations to restrict the data type. We commonly represent these type variables with single-letter names, such as:

  • E represents the element type in a collection, i.e., List.
  • R represents the return type of a function or method.
  • K represents the key type in associative collections, i.e., Map.
  • V represents the value type in associative collections, i.e., Map.

Note: We can also represent generics with descriptive names like Product or Inventory Take a look at an example of the single-letter name generics implementation.

import 'dart:collection';
//Demonstrating use of single letters for generics
//A class for grocery product
class Product {
final int id;
final double price;
final String title;
Product(this.id, this.price, this.title);
@override
String toString() {
return "Price of ${this.title} is \$${this.price}";
}
}
//A class for product's inventory
class Inventory {
final int amount;
Inventory(this.amount);
@override
String toString() {
return "Inventory amount: $amount";
}
}
//Custom type variables- Single letter
class Store<P, I> {
final HashMap<P, I> catalog = HashMap<P, I>();
List<P> get products => catalog.keys.toList();
void updateInventory(P product, I inventory) {
catalog[product] = inventory;
}
void printProducts() {
catalog.keys.forEach(
(product) => print("Product: $product, " + catalog[product].toString()),
);
}
}
//Demonstrating single letter
void main() {
Product milk = Product(1, 5.99, "Milk");
Product bread = Product(2, 4.50, "Bread");
//Using single letter names for Generics
Store<Product, Inventory> store1 = Store<Product, Inventory>();
store1.updateInventory(milk, Inventory(20));
store1.updateInventory(bread, Inventory(15));
store1.printProducts();
}

Asynchronicity in Dart#

Asynchronicity allows multiple things to happen at the same time. In Dart, asynchronous operations can perform time-consuming operations, allowing their processing to finish later.

Dart provides two ways to handle events/requests asynchronously. Dart’s dart:async library supports asynchronous programming with Future and Stream classes.

  • Future objects represent the results of asynchronous operations. They are like promises for a future result.
  • Stream objects provide a sequence of events that are either a value or an error.

Future objects#

Let’s explore future objects. Asynchronous operations results are returned as Future objects. The future object is represented as Future<T>, where T is the type of results returned. When we need the result of a completed Future, we can use either:

  • await and async
  • The Future API

We use the await and async keywords together. The function expected to perform the expensive work will be marked with the keyword async. The keyword prefixes the expensive call await inside the function.

The program will suspend when await is called, when a function returns, or when it reaches the end of the function. Let’s see an example of async and await below:

// Expensive function could be a function that takes
// long time to process data and return results.
// Assume this function takes a long time to return in real-world
String getExpansiveData() {
return "I'm expansive data";
}
// This is the asynchronous function that makes the expensive
// data calls and prints the results.
Future<void> makeDataCall() async {
var data = await getExpansiveData();
print(data);
}
//----END----//
//Entry point function
void main() {
makeDataCall();
}

Above, the Future keyword before the function makeDataCall() means this function will be executed asynchronously. Therefore, it will be suspended when it encounters await.

The makeDataCall() returns a Future of type void since there is nothing returned by the function. It calls the getExpansiveData() method with the keyword await, which returns the string I'm expansive data. The method makeDataCall() prints the results with the functionprint().

The makeDataCall() method returns a Future of type void since the function returns nothing. It calls the getExpansiveData() method with the keyword await, which returns the string I'm expansive data. The method makeDataCall() prints the results with the function print().

The Future API can also be used to execute asynchronous operations. In the Future API, the then() method registers a callback, which fires upon the completion of Future.

There are two variants of the Future API.

  • Future<String>: Future returning String data type.
  • Future<void>: Future returning void.

Conclusion and next steps#

Congrats! You should now have a good understanding of what Dart 2 brings to the table. With Dart in our Flutter projects, we can make high-quality native applications, and these new features take your Dart skills to the next level. Dart 2 introduced safer, more expressive, and flexible features, enhancing the language’s usability for web and native applications. Key updates include extensions for adding functionality, mixins for code reusability, and robust support for generics, enums, and asynchronous programming.

But there is still more to learn about Dart 2 to use this language to its full potential. Your next learning steps are the following concepts:

  • Callable classes
  • Generator Functions
  • Generic collections
  • API and Streams in Dart
  • OS Variables/Platform Class

To start with these intermediate Dart 2 concepts, check out Educative’s Developing Web Applications with Dart course. You will dive deep into Dart 2 language features with hands-on code. Gain confidence using everything from extensions to callable classes and beyond. By the end, you’ll be able to use this language in your own Flutter projects.

Happy learning!


Frequently Asked Questions

What are the features of Dart?

Dart is a versatile, object-oriented, class-based programming language designed for building high-quality web, mobile, and desktop applications. Key features include a clean syntax, strong typing, garbage collection, and the ability to compile to both native code and JavaScript. Dart 2 introduced extensions, enums, mixins, and generics for improved code reusability and type safety. It supports asynchronous programming with Future and Stream classes, making it suitable for managing concurrent operations. With these capabilities, Dart is widely used with Flutter for building performant, cross-platform applications.

What is the difference between enum and enum Dart?

How do Dart Generics work for type safe programming?

Is Dart suitable for backend web development?


Written By:
Amanda Fawcett
Join 2.5 million developers at
Explore the catalog

Free Resources