Flutter – Génération de code

Contexte

Dart, et à fortiori Flutter, ne permet pas l’introspection, c’est à dire la possibilité d’analyser les objets manipulés pendant l’exécution du projet.

Cette fonctionnalité est largement utilisée en Java au travers notamment des annotations qui vont permettre simplement de modifier le comportement d’une objet du projet.

En Flutter, les annotations reposent sur la possibilité de générer du code, cette génération devant se faire en dehors de la phase de build des projets, typiquement lors du changement d’un objet à coté duquel du code doit être généré.

Cette génération de code est notamment utilisée par la librairie json_serializable qui permet d’annoter une classe de données et d’obtenir les méthodes fromJson et toJson sur cette dernière.

Nous nous sommes servis sur de nombreux use cases de cette fonctionnalité de génération de code, mais n’avions pas eu l’occasion de créer une librairie la mettant en pratique.

Les ateliers proposés par Mobiapps nous ont semblés être le moment rêvé pour tenter l’aventure.

Présentation du sujet

Notre but était d’avoir un exemple simple mais fonctionnel qui nous permettrait de tester cet outil.

Nous avons décidé de partir du template de projet fourni par Flutter permettant sur une page d’avoir un compteur qu’on incrémente par un clic sur un bouton.

Dans ce projet, l’idée était d’inclure un BLoC prenant en charge la gestion de l’état de la page, et de générer des méthodes permettant d’incrémenter ou de décrémenter une valeur.

Le code de base est le suivant :

Dans le widget

floatingActionButton: FloatingActionButton(
  onPressed: () => _counterCubit.incrementValue(counter),
  tooltip: 'Increment',
  child: const Icon(Icons.add),
),

Dans le BLoC

void incrementValue(int value) async {
  final newValue = value++;
  emit(CounterValueState(newValue));
}

L’idée est de pouvoir générer une méthode int increment(int value) qui effectuera le calcul et pourra être accessible dans le BLoC en la mettant à disposition dans une mixin.

Mise en œuvre de la librairie

La mise en place d’un module incluant de la génération de code n’a de sens qu’au sein d’un package Flutter.Nous avons donc créé un nouveau package incluant, dans le répertoire example du projet, le code du template incluant le compteur incrémentable.

Article generation de code Flutter
Le contenu de notre projet

Dans un deuxième temps, il faut pour un package de ce type ajouter deux dépendances au projet :

  • source_gen (section dependencies) : framework mettant à disposition les outils permettant de construire les générateurs de code
  • build_runner (section dev_dependencies) : outil en ligne de commande permettant d’exécuter la génération de code proprement dite

Trois éléments seront nécessaires dans le package pour permettre l’utilisation d’annotations dans le projet cible :

- Une classe définissant l’annotation ; pour une annotation “simple”, c’est à dire sans paramètres, il suffit de définir la classe et un constructeur.

class IncDec {
  const IncDec();
}

- Une classe héritant de GeneratorForAnnotation ; la classe parent prend en type l’annotation créée et attend la redéfinition d’une méthode generateForAnnotatedElement qui fournit en sortie, en fonction de l’élément sur lequel l’annotation est posée, le code à générer.

class IncrementDecrementGenerator extends GeneratorForAnnotation<IncDec> {         
  @override         
  generateForAnnotatedElement(           
    Element element, // fourni une référence à l'objet visé par l'annotation           
    ConstantReader annotation,           
    BuildStep buildStep,         
  ) {           
    final String className = element.displayName;           
    final String output = '''             
      mixin _\$${className}Mixin {               
        int increment(int value) => value + 1;                       

        int decrement(int value) => value - 1;             
      }           
    ''';         
          
    return output;         
  }
}

- Une fonction renvoyant un objet de type Builder ; ici on va référencer la classe précédente en fournissant une instance de cette dernière aux outils de génération.

Builder incrementDecrementBuilder(BuilderOptions options) => SharedPartBuilder(         
  [IncrementDecrementGenerator()],         
  'incrementDecrement',
);

On ajoutera enfin, à la racine du projet, un fichier build.yaml permettant la configuration du build_runner (référencement des générateurs, définition de l’extension à utiliser sur les fichiers générés… cf https://github.com/dart-lang/source_gen/blob/master/example/build.yaml ).

targets:
  $default:
    builders:
      generators|annotations:
        enabled: true 

builders:
  generators:
    target: ":generators"
    import: "package:code_generator/builder.dart"
    builder_factories: ["incrementDecrementBuilder"]
    build_extensions: { ".dart": [".g.dart"] }
    auto_apply: dependents
    build_to: cache
    applies_builders: ["source_gen|combining_builder"]

Utilisation de la librairie

Une fois le code du générateur mis en place, il ne reste qu'à utiliser l’annotation dans notre projet d’exemple :

part 'counter.cubit.g.dart'; // indique le fichier à créer 

@IncDec() // annotation proprement dite
class CounterCubit extends Cubit<CubitState> with _$CounterCubitMixin { 
  // mixin générée 

... 

  void incrementValue(int value) async {
    final newValue = increment(value); 
      // la méthode increment est à disposition dans la mixin 
    emit(CounterValueState(newValue));
  }  

... 

}

Enfin on pourra générer le code en lançant la commande flutter pub run build_runner build ce qui met à disposition le code suivant :

// GENERATED CODE - DO NOT MODIFY BY HAND 

part of 'counter.cubit.dart'; 

// **************************************************************************// 
IncrementDecrementGenerator
// ************************************************************************** 

mixin _$CounterCubitMixin {
  int increment(int value) => value + 1; 

  int decrement(int value) => value - 1;
}

Template personnalisable

La redéfinition de la méthode generateForAnnotatedElement, nous permet de récupérer l’élément sur lequel l’annotation a été posée. À l’aide de ces éléments, on va construire notre template qui sera généré lors de l’exécution de la commande de build.

Ce template est représenté sous la forme d’une chaîne de caractère entièrement personnalisable. Nous avons eu l’occasion de parcourir les variables de l’élément récupéré et celui-ci est très complet. Dans l’exemple précèdent, nous avons utilisé uniquement le nom de la classe, cependant si des fonctions sont définies dans la classe héritant, chaque information de la fonction, les variables définies, leurs retours ou leurs types seront accessibles.

C’est un template qui est totalement personnalisable et qui peut totalement s’adapter au besoin, dans l’éventualité ou le template contiendrait une erreur d’implémentation, la commande de build, retourne les erreurs rencontrées.

Conclusion

La mise en place d’un générateur s’avère extrêmement simple en Flutter, nous avons pu mettre en place ce petit outil très rapidement. La construction du template généré n’est pas plus difficile, il est nécessaire de prendre le temps pour le développer et de correctement définir les variables de la classe héritant pour éviter un traitement supplémentaire lors de la génération.

Nous étudions à présent la possibilité d’utiliser cet outil pour générer, dans nos projets, une partie du code lié à notre implémentation de la clean architecture.

Vous souhaitez en savoir plus ? Contactez-nous !

Simon BERNARDIN – Lead Developer Mobile
Guillaume CAZE-FRANCOIS – Développeur mobile
Clément MARCHAIS – Développeur mobile
Bastien Civel – Développeur Fullstack