Dart extensions, generating `copyWith` methods

  • 3 min read time
  • |
  • 15 December 2019
Image not Found

As you all are probably aware that the new version of Dart 2.7 was recently released. This version introduces one particularly interesting feature called "extension methods". It allows us to extend existing classes with a new functionality. As a basic demonstration of this new feature, we can extend the List class

extension SafeLast<T> on List<T> {
  T get safeLast {
    return isEmpty ? null : last;
  }
}
 
main() {
  final list = ["test", "test1"];
  print(list.safeLast);
}

This is quite a basic example and doesn't do much. Now let's try to solve a real-life problem I am constantly stumbling upon. I am talking about implementing copyWith method on @immutable classes. This is a very popular pattern used to copy a class with some modifications. Unfortunately, it requires a lot of boilerplate code. For example:

IconThemeData copyWith({ Color color, double opacity, double size }) {
 return IconThemeData(
   color: color ?? this.color,
   opacity: opacity ?? this.opacity,
   size: size ?? this.size,
 );
}

This example is not that complex but still requires some boilerplate code. And as soon as you start adding new fields to your class, it gets messy pretty quickly. With more complex examples it can get out of control quite easily. This is something that the Dart community is aware of but the issue still exists.

A New Hope

With the new extension methods, it’s possible to define a class method aside from the main class implementation, in a separate file. This means that we can employ the code generation functionality provided by the Dart team to create this method for us.

I won’t go into details of creating a code generation package as it is a bit of a tricky topic to cover. Let's just focus on the basic idea and the actual code that generates it for us. The code generation package consists of two parts. The first part is used to mark a class with an annotation. It has only a single empty annotation class inside. And the second part contains the code generation itself, which generates code for classes marked with this annotation.

In order to generate code for a given class we do the following:
  1. Find the unnamed constructor to use with our code generation:
final constructor = element.unnamedConstructor;
  1. For every single field in this constructor we generate two types of outputs. One constructorInput to be used in the signature for our copyWith method. And another paramsInput for assigning new values to the copied class if they are provided:
final constructorInput = fields.fold(
  "",
  (r, field) => "$r ${field.type} ${field.name},",
);
final paramsInput = fields.fold(
  "",
  (r, field) => "$r ${field.name}: ${field.name} ?? this.${field.name},",
);
  1. Wrap everything into the class extension:
return '''extension ${classElement.name}CopyWithExtension on ${classElement.name} {
  ${classElement.name} copyWith({$constructorInput}) {
    return ${classElement.name}($paramsInput);
  }
}''';

This is basically it.

To use it in your project:
  1. Import these two packages into your project, along with build_runner like this:
dependencies:
  copy_with_extension: ^1.4.0
  
dev_dependencies:
  build_runner: ^1.10.3
  copy_with_extension_gen: ^1.4.0
  1. Annotate your class with @CopyWith annotation from the copy_with_extension package.
@CopyWith()
class BasicClass {
  final String id;
  BasicClass({this.id});
}
  1. Add the part annotation to tell Dart that you are going to use a part file:
part 'your_file_name.g.dart';
  1. Now you can run the code generation:
flutter pub run build_runner build

Conclusion

I am glad that the Dart team is actively improving the language itself. With this update, we can make the development process much more comfortable and our architectures cleaner. I am also very excited about the upcoming null safety which should help us to significantly improve the quality of the code. And, of course, if you want to improve this library, feel free to fire off a pull request. I will be happy to help you with this.

You May Also Like