美文网首页
Flutter使用source_gen快速提升开发效率

Flutter使用source_gen快速提升开发效率

作者: QiShare | 来源:发表于2022-03-31 15:41 被阅读0次

    认识APT

    APT(Annotation Process Tool),注解处理器,可以在编译期或运行时获取到注解信息,进行生成代码源文件、其他文件或逻辑处理的功能。

    Java中按注解保留的范围可以分为三类,功能也各不相同,分别是:

    • SOURCE:编译期间丢弃,编译完成后这些注解没有任何意义,可提供IDE语法检查,静态模版代码

      例 :@Override, @SuppressWarningsLombok

    • CLASS: 保留在class文件中,类加载期间被丢弃,运行时不可见,可以用于字节码操作、可获取到加载类信息的动态代码生成

      例:AspectJButterKnifeRoomEventBus3.0之后ARouter

    • RUNTIME:注解保留至运行期,结合反射技术使用

      例:RetrofitEventBus3.0之前

    在应用程序构建的阶段分布如图:

    image.png

    第一阶段为编译期,由外部构建工具将源代码翻译成目标可执行文件,如exe。类似嵌入式c语言开发的构建工具make、cmake,java中为javac。对应SOURCE

    第二阶段为执行期,生成的字节码.class文件是JVM可执行文件,由JVM加载.class文件、验证、执行的过程,在JVM内部完成,把.class翻译成平台相关的本地机器码。对应CLASS

    第三阶段为运行时,硬件执行机器码过程,程序运行期间。对应RUNTIME

    Flutter出于安全性考虑,不支持反射,所以本文讨论范围不包含运行时部分功能

    为什么使用代码生成

    在特定的场景下,代码自动生成有很多好处,如下几个场景:

    • 数据类(Data classes):这些类型的类相当简单,而且通常需要创建很多。因此,最好的方法是生成它们而不是手动编写每一个
    • 架构样板(Architecture boilerplate):几乎每个架构解决方案都会带有一定数量的样板代码。每次重复编写就会让人很头疼,所以,通过代码生成可以很大程度上避免这种情况。 MobX就是一个很好的这样的例子
    • 公共特性/方法(Common features/functions):几乎所有model类使用确定的方法,比如fromMap,toMap,和copyWith。通过代码可以一键生成所有这些方法

    代码生成不仅节省时间和精力,提高效率,更能提升代码质量,减少手动编写的bug数量。你可以随便打开任何生成的文件,并保证它能正常运行

    项目现状

    使用领域驱动(DDD)架构设计,核心业务逻辑层在domain层,数据获取在service层,这两层包含了稳定数据获取架构,提供了稳定性的同时,也造成了项目架构的弊病,包含大量的模版代码。

    经过多次激烈讨论,如果单纯的将servce层删掉,将势必导致domain层耦合了数据层获取的逻辑或是service层耦合底层数据池获取的逻辑,对domain层只关心核心业务和将来数据池的扩展和迁移都造成不利影响,总之,每一层都有意义。所以,最终决定保留

    不删除又会导致,实现一个功能,要编写很多代码、类。为此需要一个开发中提升效率的折中方案

    Dart运行时注解处理及代码生成库build刚好可以完成这个功能

    确定范围

    确定好Flutter支持代码生成的功能后,需要分析代码结构特点,确定使用范围

    分析代码结构

    主要业务逻辑实现分为两部分:

    1、调用接口实现的获取数据流程 
    
    2、调用物模型实现的属性服务
    

    两部分都在代码中有较高的书写频率,同时也是架构样板代码的重灾区,需要重点优化

    期望效果

    • 定义好repo层,自动生成中间层代码

    • 文件名、类名遵循架构规范

    • 移动文件到指定位置

    image.png

    困难与挑战

    • source_gen代码生成配置流程、API熟悉、调试

    • 根据注解类信息,拿到类中方法,包括方法名、返回类型、必选参数、可选参数

    • 物模型设置时,set/get方法调用不同API,返回参数为对象时,要添加convert方法自动转换

    • 接口生成类文件移动到指定目录,物模型生成文件需要拼接

    Build相关库

    类似java中的Java-APT,dart中也提供一系列注解生成代码的工具,核心库有如下几个:

    • build:提供代码生成的底层基础依赖库,定义一些创建Builder的接口
    • build_config:提供解析build.yaml文件的支持库,由build_runner使用
    • build_runner:提供了一些用于生成文件的通用命令,触发builders执行
    • source_gen:提供build库的上层封装,方便开发者使用

    生成器package配置

    快速开始:

    1、创建生成器package

    创建注解解析器的package,配置依赖

    dependency_overrides:
      build: ^2.0.0
      build_runner: ^2.0.0
      source_gen: ^0.9.1
    

    2、创建注解

    创建一个类,添加const 构造函数,可选择有参或无参:

    class Multiplier {
      final num value;
    
      const Multiplier(this.value);
    }
    

    3、创建Generator

    负责拦截解析创建的注解,创建类继承GeneratorForAnnotation<T>,实现generate方法。和Java中的Processor类似

    泛型参数是要拦截的注解,例:

    class MultiplierGenerator extends GeneratorForAnnotation<Multiplier> {
      @override
      String generateForAnnotatedElement(
        Element element,
        ConstantReader annotation,
        BuildStep buildStep,
      ) {
        final numValue = annotation.read('value').literalValue as num;
    
        return 'num ${element.name}Multiplied() => ${element.name} * $numValue;';
      }
    }
    

    返回值是String,内容就是生成的代码,可以直接返回文本,例:

    class PropertyProductGenerator extends Generator {
      @override
      String generate(LibraryReader library, BuildStep buildStep) {
        final productNames = topLevelNumVariables(library)
            .map((element) => element.name)
            .join(' * ');
    
        return '''
    num allProduct() => $productNames;
    ''';
      }
    }
    

    4、创建Builder

    Generator是通过Builder触发的,创建Builder

    Builder metadataLibraryBuilder(BuilderOptions options) => LibraryBuilder(
          MemberCountLibraryGenerator(),
          generatedExtension: '.info.dart',
        );
    Builder multiplyBuilder(BuilderOptions options) =>
        SharedPartBuilder([MultiplierGenerator()], 'multiply');
    
    

    Builder 是build 库中的抽象类

    
    /// The basic builder class, used to build new files from existing ones.
    abstract class Builder {
      /// Generates the outputs for a given [BuildStep].
      FutureOr<void> build(BuildStep buildStep);
    
      Map<String, List<String>> get buildExtensions;
    }
    

    实现类在source_gen中,对Builder进行了封装,提供更友好的API。执行Builder要依赖build_runner ,允许通过dart 代码生成文件,是编译期依赖dev_dependency;只在开发环境使用

    各个Builder作用:

    • PartBuilder:生成属于文件的part of代码。官方不推荐使用,更推荐SharedPartBuilder
    • SharedPartBuilder:生成共享的可和其他Builder合并的part of文件。比PartBuilder优势是可合并多个部分文件到最终的一个.g.dart文件输出
    • LibraryBuilder:生成单独的Dart 库文件
    • CombiningBuilder:合并其他SharedPartBuilder生产的文件。收集所有.*.g.part文件

    需要注意的是SharedPartBuilder 会生成.g.dart后缀文件输出,并且,执行命令前,要在源文件引入part '*.g.dart'才会生成文件

    LibraryBuilder,比较灵活,可以扩展任意后缀

    5、配置build.yaml

    创建的Builder要在build.yaml文件配置,build期间,会读取该文件配置,拿到自定义的Builder

    # Read about `build.yaml` at https://pub.dev/packages/build_config
    builders:
      # name of the builder
      member_count:
        # library URI containing the builder - maps to `lib/member_count_library_generator.dart`
        import: "package:source_gen_example/builder.dart"
        # Name of the function in the above library to call.
        builder_factories: ["metadataLibraryBuilder"]
        # The mapping from the source extension to the generated file extension
        build_extensions: {".dart": [".info.dart"]}
        # Will automatically run on any package that depends on it
        auto_apply: dependents
        # Generate the output directly into the package, not to a hidden cache dir
        build_to: source
        
      property_multiply:
        import: "package:source_gen_example/builder.dart"
        builder_factories: ["multiplyBuilder"]
        build_extensions: {".dart": ["multiply.g.part"]}
        auto_apply: dependents
        build_to: cache
        applies_builders: ["source_gen|combining_builder"]
    
    

    使用package配置

    1、添加依赖

    pubspec.yaml文件添加生成器package依赖。可添加到dev_dependencies

    dev_dependencies:
      source_gen_builder:
        path: ../source_gen_builder
    
    

    2、添加注解

    在要生成文件类名添加注解,这里用官方例子

    part 'library_source.g.dart';
    
    @Multiplier(2)
    const answer = 42;
    
    const tau = pi * 2;
    
    

    3、配置build.yaml

    使用的package也需要配置build.yaml,用来定制化build行为。例如,配置注解扫描范围,详情见build_config

    # Read about `build.yaml` at https://pub.dev/packages/build_config
    targets:
      $default:
        builders:
          # Configure the builder `pkg_name|builder_name`
          # In this case, the member_count builder defined in `../example`
          source_gen_builder|property_impl:
            generate_for:
    
          source_gen_builder|retrofit:
            generate_for:
              - lib/*/retrofit.dart
    
          # The end-user of a builder which applies "source_gen|combining_builder"
          # may configure the builder to ignore specific lints for their project
          source_gen|combining_builder:
            options:
              ignore_for_file:
              - lint_a
              - lint_b
    
    

    4、执行命令

    在使用的package根目录下执行:

    flutter packages pub run build_runner build 
    

    结果展示:

    生成*.g.dart文件

    // GENERATED CODE - DO NOT MODIFY BY HAND
    
    // ignore_for_file: lint_a, lint_b
    
    part of 'library_source.dart';
    
    // **************************************************************************
    // MultiplierGenerator
    // **************************************************************************
    
    num answerMultiplied() => answer * 2;
    
    

    5、debug调试

    复制该目录下文件到使用package根目录下

    image.png

    Android Studio下配置

    image.png

    点击debug按钮,打断点调试即可

    注意,debug需要生成器package和使用package在统一工程下才可以

    配合脚本使用

    上述生成文件都是带.g.dart或其他后缀文件,并且目录和源文件同级。如果想生成架构中的模版源文件,并生成到其他目录,可以配合脚本实现,可以帮你完成:后缀名修改、移动文件目录、文件代码拼接的功能

    这部分代码根据个人情况实现,大体框架如下

    #!/bin/bash
    # cd到执行目录
    cd ../packages/domain
    # 执行build命令
    flutter packages pub run build_runner build --delete-conflicting-outputs
    # 循环遍历目录下文件,
    function listFiles()
    {
            #1st param, the dir name
            #2nd param, the aligning space
            for file in `ls $1`;
            do
                    if [ -d "$1/$file" ]; then
                        listFiles "$1/$file" "$2"
                    else
                        if [[ $2$file =~ "repository.usecase.dart" ]]
                        then
                            # 找到生成对应后缀文件,执行具体操作
                            # dosmothing
                        fi
    
                        if [[ $2$file =~ "repository.impl.dart" ]]
                        then
                            # dosmothing
                        fi
    
                    fi
            done
    }
    listFiles $1 "."
    
    

    总结

    以上,就是利用Dart-APT编译期生成代码的步骤和调试过程

    最后实现的效果可以做到只声明业务层接口声明,然后脚本一键生成service中间层实现。后面再有需求过来,再也不用费力梳理架构实现逻辑和敲代码敲的手指疼了

    截止到目前,项目现在已有接口统计:GET 79、POST 97,并随着业务持续增长。从统计编码字符的维度来看,单个repo,一只接口,一个参数的情况下需手动编写222个,自动生成1725个,效率提升88.6%

    底层的数据获取使用的retrofit,同样是自动生成的代码所以不计入统计字符范围,这里的效率提升并不是指一个接口开发完成的整体效率,而是只涵盖从领域到数据获取中间层的代码编写效率

    字符和行数优化前后对比:

    image.png

    达到了既保证不破坏项目架构,又提升开发效率的目标

    相关文章

      网友评论

          本文标题:Flutter使用source_gen快速提升开发效率

          本文链接:https://www.haomeiwen.com/subject/garnjrtx.html