27 min read

In this chapter, we will discuss the flexibility and reusability of your code with the help of advanced techniques in Dart. Generic programming is widely useful and is about making your code type-unaware. Using types and generics makes your code safer and allows you to detect bugs early. The debate over errors versus exceptions splits developers into two sides. Which side to choose? It doesn’t matter if you know the secret of using both. Annotation is another advanced technique used to decorate existing classes at runtime to change their behavior. Annotations can help reduce the amount of boilerplate code to write your applications. And last but not least, we will open Pandora’s box through Mirrors of reflection. In this chapter, we will cover the following topics:

  • Generics
  • Errors versus exceptions
  • Annotations
  • Reflection

Generics

Dart originally came with generics—a facility of generic programming. We have to tell the static analyzer the permitted type of a collection so it can inform us at compile time if we insert a wrong type of object. As a result, programs become clearer and safer to use. We will discuss how to effectively use generics and minimize the complications associated with them.

Raw types

Dart supports arrays in the form of the List class. Let’s say you use a list to store data. The data that you put in the list depends on the context of your code. The list may contain different types of data at the same time, as shown in the following code:

// List of data

List raw = [1, “Letter”, {‘test’:’wrong’}];

// Ordinary item

double item = 1.23;

 

void main() {

// Add the item to array

raw.add(item);

print(raw);

}

In the preceding code, we assigned data of different types to the raw list.
When the code executes, we get the following result:

[1, Letter, {test: wrong}, 1.23]

So what’s the problem with this code? There is no problem. In our code, we intentionally used the default raw list class in order to store items of different types. But such situations are very rare. Usually, we keep data of a specific type in a list. How can we prevent inserting the wrong data type into the list? One way is to
check the data type each time we read or write data to the list, as shown in the following code:

// Array of String data

List parts = [‘wheel’, ‘bumper’, ‘engine’];

// Ordinary item

double item = 1.23;

 

void main() {

if (item is String) {

   // Add the item to array

   parts.add(item);

}

print(parts);

}

Now, from the following result, we can see that the code is safer and works
as expected:

[wheel, bumper, engine]

The code becomes more complicated with those extra conditional statements.
What should you do when you add the wrong type in the list and it throws exceptions? What if you forget to insert an extra conditional statement?
This is where generics come to the fore.

Instead of writing a lot of type checks and class casts when manipulating a collection, we tell the static analyzer what type of object the list is allowed to contain. Here is the modified code, where we specify that parts can only contain strings:

// Array of String data

List<String> parts = [‘wheel’, ‘bumper’, ‘engine’];

// Ordinary item

double item = 1.23;

 

void main() {

// Add the item to array

parts.add(item);

print(parts);

}

Now, List is a generic class with the String parameter. Dart Editor invokes the static analyzer to check the types in the code for potential problems at compile time and alert us if we try to insert a wrong type of object in our collection, as shown in the following screenshot:

 

This helps us make the code clearer and safer because the static analyzer checks the type of the collection at compile time. The important point is that you shouldn’t use raw types. As a bonus, we can use a whole bunch of shorthand methods to organize iteration through the list of items to cast safer. Bear in mind that the static analyzer only warns about potential problems and doesn’t generate any errors.

 

 

Dart checks the types of generic classes only in the check mode. Execution in the production mode or code compiled to JavaScript loses all the type information.

 

 

Using generics

Let’s discuss how to make the transition to using generics in our code with some real-world examples. Assume that we have the following AssemblyLine class:

part of assembly.room;

 

// AssemblyLine.

class AssemblyLine {

// List of items on line.

List _items = [];

// Add [item] to line.

add(item) {

   _items.add(item);

}

 

// Make operation on all items in line.

make(operation) {

   _items.forEach((item) {

     operation(item);

   });

}

}

Also, we have a set of different kinds of cars, as shown in the following code:

part of assembly.room;

 

// Car

abstract class Car {

// Color

String color;

}

 

// Passenger car

class PassengerCar extends Car {

String toString() => “Passenger Car”;

}

 

// Truck

class Truck extends Car {

String toString() => “Truck”;

}

Finally, we have the following assembly.room library with a main method:

library assembly.room;

 

part ‘assembly_line.dart’;

part ‘car.dart’;

 

operation(car) {

print(‘Operate ${car}’);

}

 

main() {

// Create passenger assembly line

AssemblyLine passengerCarAssembly = new AssemblyLine();

// We can add passenger car

passengerCarAssembly.add(new PassengerCar());

// We can occasionally add Truck as well

passengerCarAssembly.add(new Truck());

// Operate

passengerCarAssembly.make(operation);

}

In the preceding example, we were able to add the occasional truck in the assembly line for passenger cars without any problem to get the following result:

Operate Passenger Car

Operate Truck

This seems a bit far fetched since in real life, we can’t assemble passenger cars and trucks in the same assembly line. So to make your solution safer, you need to make the AssemblyLine type generic.

Generic types

In general, it’s not difficult to make a type generic. Consider the following example of the AssemblyLine class:

part of assembly.room;

 

// AssemblyLine.

class AssemblyLine <E extends Car> {

// List of items on line.

List<E> _items = [];

// Add [item] to line.

add(E item) {

   _items.insert(0, item);

}

// Make operation on all items in line.

make(operation) {

   _items.forEach((E item) {

     operation(item);

   });

}

}

In the preceding code, we added one type parameter, E, in the declaration of the AssemblyLine class. In this case, the type parameter requires the original one to be a subtype of Car. This allows the AssemblyLine implementation to take advantage of Car without the need for casting a class. The type parameter E is known as a bounded type parameter. Any changes to the assembly.room library will look like this:

library assembly.room;

 

part ‘assembly_line.dart’;

part ‘car.dart’;

 

operation(car) {

print(‘Operate ${car}’);

}

 

main() {

// Create passenger assembly line

AssemblyLine<PassengerCar> passengerCarAssembly =

     new AssemblyLine<PassengerCar>();

// We can add passenger car

passengerCarAssembly.add(new PassengerCar());

// We can occasionally add truck as well

passengerCarAssembly.add(new Truck());

// Operate

passengerCarAssembly.make(operation);

}

The static analyzer alerts us at compile time if we try to insert the Truck argument in the assembly line for passenger cars, as shown in the following screenshot:

 

After we fix the code in line 17, all looks good. Our assembly line is now safe. But if you look at the operation function, it is totally different for passenger cars than it is for trucks; this means that we must make the operation generic as well. The static analyzer doesn’t show any warnings and, even worse, we cannot make the operation generic directly because Dart doesn’t support generics for functions. But there is
a solution.

Generic functions

Functions, like all other data types in Dart, are objects, and they have the data type Function. In the following code, we will create an Operation class as an implementation of Function and then apply generics to it as usual:

part of assembly.room;

 

// Operation for specific type of car

class Operation<E extends Car> implements Function {

// Operation name

final String name;

// Create new operation with [name]

Operation(this.name);

// We call our function here

call(E car) {

   print(‘Make ${name} on ${car}’);

}

}

The gem in our class is the call method. As Operation implements Function and has a call method, we can pass an instance of our class as a function in the make method of the assembly line, as shown in the following code:

library assembly.room;

 

part ‘assembly.dart’;

part ‘car.dart’;

part ‘operation.dart’;

 

main() {

// Paint operation for passenger car

Operation<PassengerCar> paint = new  

   Operation<PassengerCar>(“paint”);

// Paint operation for Trucks

Operation<Truck> paintTruck = new Operation<Truck>(“paint”);

// Create passenger assembly line

Assembly<PassengerCar> passengerCarAssembly =

   new Assembly<PassengerCar>();

// We can add passenger car

passengerCarAssembly.add(new PassengerCar());

// Operate only with passenger car

passengerCarAssembly.make(paint);

// Operate with mistake

passengerCarAssembly.make(paintTruck);

}

In the preceding code, we created the paint operation to paint the passenger cars and the paintTruck operation to paint trucks. Later, we created the passengerCarAssembly line and added a new passenger car to the line via the add method. We can run the paint operation on the passenger car by calling the make method of the passengerCarAssembly line. Next, we intentionally made a mistake and tried to paint the truck on the assembly line for passenger cars, which resulted in the following runtime exception:

Make paint on Passenger Car

Unhandled exception:

type ‘PassengerCar’ is not a subtype of type ‘Truck’ of ‘car’.

#0 Operation.call (…/generics_operation.dart:10:10)

#1 Assembly.make.<anonymous  

closure>(…/generics_assembly.dart:16:15)

#2 List.forEach (dart:core-patch/growable_array.dart:240)

#3 Assembly.make (…/generics_assembly.dart:15:18)

#4 main (…/generics_assembly_and_operation_room.dart:20:28)

This trick with the call method of the Function type helps you make all the aspects of your assembly line generic. We’ve seen how to make a class generic and function to make the code of our application safer and cleaner.

 

 

The documentation generator automatically adds information about generics in the generated documentation pages.

 

 

To understand the differences between errors and exceptions, let’s move on to the next topic.

Errors versus exceptions

Runtime faults can and do occur during the execution of a Dart program.
We can split all faults into two types:

  • Errors
  • Exceptions

There is always some confusion on deciding when to use each kind of fault, but you will be given several general rules to make your life a bit easier. All your decisions will be based on the simple principle of recoverability. If your code generates a fault that can reasonably be recovered from, use exceptions. Conversely, if the code generates a fault that cannot be recovered from, or where continuing the execution would do more harm, use errors.

Let’s take a look at each of them in detail.

Errors

An error occurs if your code has programming errors that should be fixed by the programmer. Let’s take a look at the following main function:

main() {

// Fixed length list

List list = new List(5);

// Fill list with values

for (int i = 0; i < 10; i++) {

   list[i] = i;

}

print(‘Result is ${list}’);

}

We created an instance of the List class with a fixed length and then tried to fill it with values in a loop with more items than the fixed size of the List class. Executing the preceding code generates RangeError, as shown in the following screenshot:

 

This error occurred because we performed a precondition violation in our code when we tried to insert a value in the list at an index outside the valid range. Mostly, these types of failures occur when the contract between the code and the calling API is broken. In our case, RangeError indicates that the precondition was violated. There are a whole bunch of errors in the Dart SDK such as CastError, RangeError, NoSuchMethodError, UnsupportedError, OutOfMemoryError, and StackOverflowError. Also, there are many others that you will find in the errors.dart file as a part of the dart.core library. All error classes inherit from the Error class and can return stack trace information to help find the bug quickly. In the preceding example, the error happened in line 6 of the main method in the
range_error.dart file.

We can catch errors in our code, but because the code was badly implemented,
we should rather fix it. Errors are not designed to be caught, but we can throw
them if a critical situation occurs. A Dart program should usually terminate
when an error occurs.

Exceptions

Exceptions, unlike errors, are meant to be caught and usually carry information about the failure, but they don’t include the stack trace information. Exceptions happen in recoverable situations and don’t stop the execution of a program. You can throw any non-null object as an exception, but it is better to create a new exception class that implements the abstract class Exception and overrides the toString method of the Object class in order to deliver additional information. An exception should be handled in a catch clause or made to propagate outwards. The following is an example of code without the use of exceptions:

import ‘dart:io’;

 

main() {

// File URI

Uri uri = new Uri.file(“test.json”);

// Check uri

if (uri != null) {

   // Create the file

   File file = new File.fromUri(uri);

   // Check whether file exists

   if (file.existsSync()) {

     // Open file

     RandomAccessFile random = file.openSync();

     // Check random

     if (random != null) {

       // Read file

       List<int> notReadyContent =

         random.readSync(random.lengthSync());

        // Check not ready content

       if (notReadyContent != null) {

         // Convert to String

         String content = new

           String.fromCharCodes(notReadyContent);

         // Print results

         print(‘File content: ${content}’);

       }

       // Close file

       random.closeSync();

     }

   } else {

     print (“File doesn’t exist”);

   }

}

}

Here is the result of this code execution:

File content: [{ name: Test, length: 100 }]

As you can see, the error detection and handling leads to a confusing spaghetti code. Worse yet, the logical flow of the code has been lost, making it difficult to read and understand it. So, we transform our code to use exceptions as follows:

import ‘dart:io’;

 

main() {

RandomAccessFile random;

try {

   // File URI

   Uri uri = new Uri.file(“test.json”);

   // Create the file

   File file = new File.fromUri(uri);

   // Open file

   random = file.openSync();

   // Read file

   List<int> notReadyContent =

    random.readSync(random.lengthSync());

   // Convert to String

   String content = new String.fromCharCodes(notReadyContent);

   // Print results

   print(‘File content: ${content}’);

} on ArgumentError catch(ex) {

   print(‘Argument error exception’);

} on UnsupportedError catch(ex) {

   print(‘URI cannot reference a file’);

} on FileSystemException catch(ex) {

   print (“File doesn’t exist or accessible”);

} finally {

   try {

     random.closeSync();

   } on FileSystemException catch(ex) {

     print(“File can’t be close”);

   }

}

}

The code in the finally statement will always be executed independent of
whether the exception happened or not to close the random file. Finally, we have a clear separation of exception handling from the working code and we can now propagate uncaught exceptions outwards in the call stack.

The suggestions based on recoverability after exceptions are fragile. In our example, we caught ArgumentError and UnsupportError in common with FileSystemException. This was only done to show that errors and exceptions
have the same nature and can be caught any time. So, what is the truth? While developing my own framework, I used the following principle:

If I believe the code cannot recover, I use an error, and if I think it can recover,
I use an exception.

Let’s discuss another advanced technique that has become very popular and that helps you change the behavior of the code without making any changes to it.

Annotations

An annotation is metadata—data about data. An annotation is a way to keep additional information about the code in the code itself. An annotation can have parameter values to pass specific information about an annotated member.
An annotation without parameters is called a marker annotation. The purpose
of a marker annotation is just to mark the annotated member.

Dart annotations are constant expressions beginning with the @ character. We can apply annotations to all the members of the Dart language, excluding comments
and annotations themselves. Annotations can be:

  • Interpreted statically by parsing the program and evaluating the constants via a suitable interpreter
  • Retrieved via reflection at runtime by a framework

 

 

The documentation generator does not add annotations to the generated documentation pages automatically, so the information about annotations must be specified separately in comments.

 

 

Built-in annotations

There are several built-in annotations defined in the Dart SDK interpreted by the static analyzer. Let’s take a look at them.

Deprecated

The first built-in annotation is deprecated, which is very useful when you need to mark a function, variable, a method of a class, or even a whole class as deprecated and that it should no longer be used. The static analyzer generates a warning whenever a marked statement is used in code, as shown in the following screenshot:

 

Override

Another built-in annotation is override. This annotation informs the static analyzer that any instance member, such as a method, getter, or setter, is meant to override the member of a superclass with the same name. The class instance variables as well as static members never override each other. If an instance member marked with override fails to correctly override a member in one of its superclasses, the static analyzer generates the following warning:

 

Proxy

The last annotation is proxy. Proxy is a well-known pattern used when we need to call a real class’s methods through the instance of another class. Let’s assume that
we have the following Car class:

part of cars;

 

// Class Car

class Car {

int _speed = 0;

// The car speed

int get speed => _speed;

// Accelerate car

accelerate(acc) {

   _speed += acc;

}

}

To drive the car instance, we must accelerate it as follows:

library cars;

 

part ‘car.dart’;

 

main() {

Car car = new Car();

car.accelerate(10);

print(‘Car speed is ${car.speed}’);

}

We now run our example to get the following result:

Car speed is 10

In practice, we may have a lot of different car types and would want to test all of them. To help us with this, we created the CarProxy class by passing an instance of Car in the proxy’s constructor. From now on, we can invoke the car’s methods through the proxy and save the results in a log as follows:

part of cars;

 

// Proxy to [Car]

class CarProxy {

final Car _car;

// Create new proxy to [car]

CarProxy(this._car);

@override

noSuchMethod(Invocation invocation) {

   if (invocation.isMethod &&

       invocation.memberName == const Symbol(‘accelerate’)) {

     // Get acceleration value

     var acc = invocation.positionalArguments[0];

     // Log info

     print(“LOG: Accelerate car with ${acc}”);

     // Call original method

     _car.accelerate(acc);

   } else if (invocation.isGetter &&

               invocation.memberName == const Symbol(‘speed’)) {

     var speed = _car.speed;

     // Log info

     print(“LOG: The car speed ${speed}”);

     return speed;

   }

   return super.noSuchMethod(invocation);

}

}

As you can see, CarProxy does not implement the Car interface. All the magic happens inside noSuchMethod, which is overridden from the Object class. In this method, we compare the invoked member name with accelerate and speed. If the comparison results match one of our conditions, we log the information and then call the original method on the real object. Now let’s make changes to the main method, as shown in the following screenshot:

 

Here, the static analyzer alerts you with a warning because the CarProxy class doesn’t have the accelerate method and the speed getter. You must add the
proxy annotation to the definition of the CarProxy class to suppress the static analyzer warning, as shown in the following screenshot:

 

Now with all the warnings gone, we can run our example to get the following successful result:

Car speed is 10

LOG: Accelerate car with 10

LOG: The car speed 20

Car speed through proxy is 20

Custom annotations

Let’s say we want to create a test framework. For this, we will need several
custom annotations to mark methods in a testable class to be included in a test case.
The following code has two custom annotations. In the case, where we need only marker annotation, we use a constant string test. In the event that we need to pass parameters to an annotation, we will use a Test class with a constant constructor,
as shown in the following code:

library test;

 

// Marker annotation test

const String test = “test”;

 

// Test annotation

class Test {

// Should test be ignored?

final bool include;

// Default constant constructor

const Test({this.include:true});

 

String toString() => ‘test’;

}

The Test class has the final include variable initialized with a default value of
true. To exclude a method from tests, we should pass false as a parameter for
the annotation, as shown in the following code:

library test.case;

 

import ‘test.dart’;

import ‘engine.dart’;

 

// Test case of Engine

class TestCase {

Engine engine = new Engine();

// Start engine

@test

testStart() {

   engine.start();

   if (!engine.started) throw new Exception(“Engine must start”);

}

// Stop engine

@Test()

testStop() {

   engine.stop();

   if (engine.started) throw new Exception(“Engine must stop”);

}

// Warm up engine

@Test(include:false)

testWarmUp() {

   // …

}

}

In this scenario, we test the Engine class via the invocation of the testStart
and testStop methods of TestCase, while avoiding the invocation of the testWarmUp method.

So what’s next? How can we really use annotations? Annotations are useful with reflection at runtime, so now it’s time to discuss how to make annotations available through reflection.

Reflection

Introspection is the ability of a program to discover and use its own structure. Reflection is the ability of a program to use introspection to examine and modify the structure and behavior of the program at runtime. You can use reflection to dynamically create an instance of a type or get the type from an existing object
and invoke its methods or access its fields and properties. This makes your code
more dynamic and can be written against known interfaces so that the actual classes can be instantiated using reflection. Another purpose of reflection is to create development and debugging tools, and it is also used for meta-programming.

There are two different approaches to implementing reflection:

  • The first approach is that the information about reflection is tightly integrated with the language and exists as part of the program’s structure. Access to program-based reflection is available by a property or method.
  • The second approach is based on the separation of reflection information and program structure. Reflection information is separated inside a distinct Mirror object that binds to the real program member.

Dart reflection follows the second approach with Mirrors. You can find more information about the concept of Mirrors in the original paper written by Gilad Bracha at http://bracha.org/mirrors.pdf. Let’s discuss the advantages
of Mirrors:

  • Mirrors are separate from the main code and cannot be exploited for malicious purposes
  • As reflection is not part of the code, the resulting code is smaller
  • There are no method-naming conflicts between the reflection API and inspected classes
  • It is possible to implement many different Mirrors with different levels of reflection privileges
  • It is possible to use Mirrors in command-line and web applications

Let’s try Mirrors and see what we can do with them. We will continue to create a library to run our tests.

Introspection in action

We will demonstrate the use of Mirrors with something simple such as introspection. We will need a universal code that can retrieve the information about any object or class in our program to discover its structure and possibly manipulate it with properties and call methods. For this, we’ve prepared the TypeInspector class.
Let’s take a look at the code. We’ve imported the dart:mirrors library here to
add the introspection ability to our code:

library inspector;

 

import ‘dart:mirrors’;

import ‘test.dart’;

 

class TypeInspector {

ClassMirror _classMirror;

// Create type inspector for [type].

TypeInspector(Type type) {

   _classMirror = reflectClass(type);

}

The ClassMirror class contains all the information about the observing type. We perform the actual introspection with the reflectClass function of Mirrors and return a distinct Mirror object as the result. Then, we call the getAnnotatedMethods method and specify the name of the annotation that we are interested in. This will return a list of MethodMirror that will contain methods annotated with specified parameters. One by one, we step through all the instance members and call the private _isMethodAnnotated method. If the result of the execution of the _isMethodAnnotated method is successful, then we add the discovering method to the result list of found MethodMirror’s, as shown in the following code:

// Return list of method mirrors assigned by [annotation].

List<MethodMirror> getAnnotatedMethods(String annotation) {

   List<MethodMirror> result = [];

   // Get all methods

   _classMirror.instanceMembers.forEach(

     (Symbol name, MethodMirror method) {

     if (_isMethodAnnotated(method, annotation)) {

       result.add(method);

     }

   });

   return result;

}

The first argument of _isMethodAnnotated has the metadata property that keeps a list of annotations. The second argument of this method is the annotation name that we would like to find. The inst variable holds a reference to the original object in the reflectee property. We pass through all the method’s metadata to exclude some of them annotated with the Test class and marked with include equals false. All other method’s annotations should be compared to the annotation name, as follows:

// Check is [method] annotated with [annotation].

bool _isMethodAnnotated(MethodMirror method, String annotation) {

   return method.metadata.any(

     (InstanceMirror inst) {

     // For [Test] class we check include condition

     if (inst.reflectee is Test &&

       !(inst.reflectee as Test).include) {

       // Test must be exclude

       return false;

     }

     // Literal compare of reflectee and annotation

     return inst.reflectee.toString() == annotation;

   });

}

}

Dart Mirrors have the following three main functions for introspection:

  • reflect: This function is used to introspect an instance that is passed as a parameter and saves the result in InstanceMirror or ClosureMirror. For the first one, we can call methods, functions, or get and set fields of the reflectee property. For the second one, we can execute the closure.
  • reflectClass: This function reflects the class declaration and returns ClassMirror. It holds full information about the type passed as a parameter.
  • reflectType: This function returns TypeMirror and reflects a class, typedef, function type, or type variable.

Let’s take a look at the main code:

library test.framework;

 

import ‘type_inspector.dart’;

import ‘test_case.dart’;

 

main() {

TypeInspector inspector = new TypeInspector(TestCase);

List methods = inspector.getAnnotatedMethods(‘test’);

print(methods);

}

Firstly, we created an instance of our TypeInspector class and passed the testable class, in our case, TestCase. Then, we called getAnnotatedMethods from inspector with the name of the annotation, test. Here is the result of the execution:

[MethodMirror on ‘testStart’, MethodMirror on ‘testStop’]

The inspector method found the methods testStart and testStop and ignored testWarmUp of the TestCase class as per our requirements.

Reflection in action

We have seen how introspection helps us find methods marked with annotations. Now we need to call each marked method to run the actual tests. We will do that using reflection. Let’s make a MethodInvoker class to show reflection in action:

library executor;

 

import ‘dart:mirrors’;

 

class MethodInvoker implements Function {

// Invoke the method

call(MethodMirror method) {

   ClassMirror classMirror = method.owner as ClassMirror;

   // Create an instance of class

   InstanceMirror inst =

     classMirror.newInstance(new Symbol(”), []);

   // Invoke method of instance

   inst.invoke(method.simpleName, []);

}

}

As the MethodInvoker class implements the Function interface and has the call method, we can call instance it as if it was a function. In order to call the method,
we must first instantiate a class. Each MethodMirror method has the owner property, which points to the owner object in the hierarchy. The owner of MethodMirror in our case is ClassMirror. In the preceding code, we created a new instance of the class with an empty constructor and then we invoked the method of inst by name. In both cases, the second parameter was an empty list of method parameters.

Now, we introduce MethodInvoker to the main code. In addition to TypeInspector, we create the instance of MethodInvoker. One by one, we step through the methods and send each of them to invoker. We print Success only if no exceptions occur.
To prevent the program from terminating if any of the tests failed, we wrap invoker in the try-catch block, as shown in the following code:

library test.framework;

 

import ‘type_inspector.dart’;

import ‘method_invoker.dart’;

import ‘engine_case.dart’;

 

main() {

TypeInspector inspector = new TypeInspector(TestCase);

List methods = inspector.getAnnotatedMethods(test);

MethodInvoker invoker = new MethodInvoker();

methods.forEach((method) {

   try {

     invoker(method);

     print(‘Success ${method.simpleName}’);

   } on Exception catch(ex) {

     print(ex);

   } on Error catch(ex) {

     print(“$ex : ${ex.stackTrace}”);

   }

});

}

As a result, we will get the following code:

Success Symbol(“testStart”)

Success Symbol(“testStop”)

To prove that the program will not terminate in the case of an exception in the tests, we will change the code in TestCase to break it, as follows:

// Start engine

@test

testStart() {

engine.start();

// !!! Broken for reason

if (engine.started) throw new Exception(“Engine must start”);

}

When we run the program, the code for testStart fails, but the program continues executing until all the tests are finished, as shown in the following code:

Exception: Engine must start

Success Symbol(“testStop”)

And now our test library is ready for use. It uses introspection and reflection to observe and invoke marked methods of any class.

Summary

This concludes mastering of the advanced techniques in Dart. You now know that generics produce safer and clearer code, annotation with reflection helps execute code dynamically, and errors and exceptions play an important role in finding bugs that are detected at runtime.

In the next chapter, we will talk about the creation of objects and how and when to create them using best practices from the programming world.

 

LEAVE A REPLY

Please enter your comment!
Please enter your name here