Chapter 2. Advanced Techniques and Reflection

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:

Raw types

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.

Note

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:

Generic types

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.

Note

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.184.102