Flutter使用source_gen快速提升開發(fā)效率

認識APT

APT(Annotation Process Tool),注解處理器寓娩,可以在編譯期或運行時獲取到注解信息吭历,進行生成代碼源文件堕仔、其他文件或邏輯處理的功能。

Java中按注解保留的范圍可以分為三類晌区,功能也各不相同摩骨,分別是:

  • SOURCE:編譯期間丟棄,編譯完成后這些注解沒有任何意義朗若,可提供IDE語法檢查恼五,靜態(tài)模版代碼

    例 :@Override, @SuppressWarningsLombok

  • CLASS: 保留在class文件中哭懈,類加載期間被丟棄灾馒,運行時不可見,可以用于字節(jié)碼操作遣总、可獲取到加載類信息的動態(tài)代碼生成

    例:AspectJ睬罗、ButterKnife轨功、RoomEventBus3.0之后傅物、ARouter

  • RUNTIME:注解保留至運行期夯辖,結(jié)合反射技術使用

    例:RetrofitEventBus3.0之前

在應用程序構(gòu)建的階段分布如圖:

image.png

第一階段為編譯期董饰,由外部構(gòu)建工具將源代碼翻譯成目標可執(zhí)行文件蒿褂,如exe。類似嵌入式c語言開發(fā)的構(gòu)建工具make卒暂、cmake啄栓,java中為javac。對應SOURCE

第二階段為執(zhí)行期也祠,生成的字節(jié)碼.class文件是JVM可執(zhí)行文件昙楚,由JVM加載.class文件、驗證诈嘿、執(zhí)行的過程堪旧,在JVM內(nèi)部完成,把.class翻譯成平臺相關的本地機器碼奖亚。對應CLASS

第三階段為運行時淳梦,硬件執(zhí)行機器碼過程,程序運行期間昔字。對應RUNTIME

Flutter出于安全性考慮爆袍,不支持反射,所以本文討論范圍不包含運行時部分功能

為什么使用代碼生成

在特定的場景下作郭,代碼自動生成有很多好處陨囊,如下幾個場景:

  • 數(shù)據(jù)類(Data classes):這些類型的類相當簡單,而且通常需要創(chuàng)建很多夹攒。因此蜘醋,最好的方法是生成它們而不是手動編寫每一個
  • 架構(gòu)樣板(Architecture boilerplate):幾乎每個架構(gòu)解決方案都會帶有一定數(shù)量的樣板代碼。每次重復編寫就會讓人很頭疼咏尝,所以堂湖,通過代碼生成可以很大程度上避免這種情況。 MobX就是一個很好的這樣的例子
  • 公共特性/方法(Common features/functions):幾乎所有model類使用確定的方法状土,比如fromMap,toMap,和copyWith无蜂。通過代碼可以一鍵生成所有這些方法

代碼生成不僅節(jié)省時間和精力,提高效率蒙谓,更能提升代碼質(zhì)量斥季,減少手動編寫的bug數(shù)量。你可以隨便打開任何生成的文件,并保證它能正常運行

項目現(xiàn)狀

使用領域驅(qū)動(DDD)架構(gòu)設計酣倾,核心業(yè)務邏輯層在domain層舵揭,數(shù)據(jù)獲取在service層,這兩層包含了穩(wěn)定數(shù)據(jù)獲取架構(gòu)躁锡,提供了穩(wěn)定性的同時午绳,也造成了項目架構(gòu)的弊病,包含大量的模版代碼映之。

經(jīng)過多次激烈討論拦焚,如果單純的將servce層刪掉,將勢必導致domain層耦合了數(shù)據(jù)層獲取的邏輯或是service層耦合底層數(shù)據(jù)池獲取的邏輯杠输,對domain層只關心核心業(yè)務和將來數(shù)據(jù)池的擴展和遷移都造成不利影響赎败,總之,每一層都有意義蠢甲。所以僵刮,最終決定保留

不刪除又會導致,實現(xiàn)一個功能鹦牛,要編寫很多代碼搞糕、類。為此需要一個開發(fā)中提升效率的折中方案

Dart運行時注解處理及代碼生成庫build剛好可以完成這個功能

確定范圍

確定好Flutter支持代碼生成的功能后曼追,需要分析代碼結(jié)構(gòu)特點窍仰,確定使用范圍

分析代碼結(jié)構(gòu)

主要業(yè)務邏輯實現(xiàn)分為兩部分:

1、調(diào)用接口實現(xiàn)的獲取數(shù)據(jù)流程 

2拉鹃、調(diào)用物模型實現(xiàn)的屬性服務

兩部分都在代碼中有較高的書寫頻率,同時也是架構(gòu)樣板代碼的重災區(qū)鲫忍,需要重點優(yōu)化

期望效果

  • 定義好repo層膏燕,自動生成中間層代碼

  • 文件名、類名遵循架構(gòu)規(guī)范

  • 移動文件到指定位置

image.png

困難與挑戰(zhàn)

  • source_gen代碼生成配置流程悟民、API熟悉坝辫、調(diào)試

  • 根據(jù)注解類信息,拿到類中方法射亏,包括方法名近忙、返回類型、必選參數(shù)智润、可選參數(shù)

  • 物模型設置時及舍,set/get方法調(diào)用不同API,返回參數(shù)為對象時窟绷,要添加convert方法自動轉(zhuǎn)換

  • 接口生成類文件移動到指定目錄锯玛,物模型生成文件需要拼接

Build相關庫

類似java中的Java-APT,dart中也提供一系列注解生成代碼的工具,核心庫有如下幾個:

  • build:提供代碼生成的底層基礎依賴庫,定義一些創(chuàng)建Builder的接口
  • build_config:提供解析build.yaml文件的支持庫攘残,由build_runner使用
  • build_runner:提供了一些用于生成文件的通用命令拙友,觸發(fā)builders執(zhí)行
  • source_gen:提供build庫的上層封裝,方便開發(fā)者使用

生成器package配置

快速開始:

1歼郭、創(chuàng)建生成器package

創(chuàng)建注解解析器的package遗契,配置依賴

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

2、創(chuàng)建注解

創(chuàng)建一個類病曾,添加const 構(gòu)造函數(shù)牍蜂,可選擇有參或無參:

class Multiplier {
  final num value;

  const Multiplier(this.value);
}

3、創(chuàng)建Generator

負責攔截解析創(chuàng)建的注解知态,創(chuàng)建類繼承GeneratorForAnnotation<T>捷兰,實現(xiàn)generate方法。和Java中的Processor類似

泛型參數(shù)是要攔截的注解负敏,例:

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贡茅,內(nèi)容就是生成的代碼,可以直接返回文本其做,例:

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顶考、創(chuàng)建Builder

Generator是通過Builder觸發(fā)的,創(chuàng)建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;
}

實現(xiàn)類在source_gen中妖泄,對Builder進行了封裝驹沿,提供更友好的API。執(zhí)行Builder要依賴build_runner 蹈胡,允許通過dart 代碼生成文件渊季,是編譯期依賴dev_dependency;只在開發(fā)環(huán)境使用

各個Builder作用:

  • PartBuilder:生成屬于文件的part of代碼罚渐。官方不推薦使用却汉,更推薦SharedPartBuilder
  • SharedPartBuilder:生成共享的可和其他Builder合并的part of文件。比PartBuilder優(yōu)勢是可合并多個部分文件到最終的一個.g.dart文件輸出
  • LibraryBuilder:生成單獨的Dart 庫文件
  • CombiningBuilder:合并其他SharedPartBuilder生產(chǎn)的文件荷并。收集所有.*.g.part文件

需要注意的是SharedPartBuilder 會生成.g.dart后綴文件輸出合砂,并且,執(zhí)行命令前源织,要在源文件引入part '*.g.dart'才會生成文件

LibraryBuilder翩伪,比較靈活,可以擴展任意后綴

5谈息、配置build.yaml

創(chuàng)建的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窑眯、執(zhí)行命令

在使用的package根目錄下執(zhí)行:

flutter packages pub run build_runner build 

結(jié)果展示:

生成*.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調(diào)試

復制該目錄下文件到使用package根目錄下

image.png

Android Studio下配置

image.png

點擊debug按鈕医窿,打斷點調(diào)試即可

注意磅甩,debug需要生成器package和使用package在統(tǒng)一工程下才可以

配合腳本使用

上述生成文件都是帶.g.dart或其他后綴文件,并且目錄和源文件同級姥卢。如果想生成架構(gòu)中的模版源文件卷要,并生成到其他目錄,可以配合腳本實現(xiàn)独榴,可以幫你完成:后綴名修改僧叉、移動文件目錄、文件代碼拼接的功能

這部分代碼根據(jù)個人情況實現(xiàn)棺榔,大體框架如下

#!/bin/bash
# cd到執(zhí)行目錄
cd ../packages/domain
# 執(zhí)行build命令
flutter packages pub run build_runner build --delete-conflicting-outputs
# 循環(huán)遍歷目錄下文件瓶堕,
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
                        # 找到生成對應后綴文件,執(zhí)行具體操作
                        # dosmothing
                    fi

                    if [[ $2$file =~ "repository.impl.dart" ]]
                    then
                        # dosmothing
                    fi

                fi
        done
}
listFiles $1 "."

總結(jié)

以上症歇,就是利用Dart-APT編譯期生成代碼的步驟和調(diào)試過程

最后實現(xiàn)的效果可以做到只聲明業(yè)務層接口聲明郎笆,然后腳本一鍵生成service中間層實現(xiàn)。后面再有需求過來忘晤,再也不用費力梳理架構(gòu)實現(xiàn)邏輯和敲代碼敲的手指疼了

截止到目前宛蚓,項目現(xiàn)在已有接口統(tǒng)計:GET 79、POST 97德频,并隨著業(yè)務持續(xù)增長苍息。從統(tǒng)計編碼字符的維度來看缩幸,單個repo壹置,一只接口,一個參數(shù)的情況下需手動編寫222個表谊,自動生成1725個钞护,效率提升88.6%

底層的數(shù)據(jù)獲取使用的retrofit,同樣是自動生成的代碼所以不計入統(tǒng)計字符范圍爆办,這里的效率提升并不是指一個接口開發(fā)完成的整體效率难咕,而是只涵蓋從領域到數(shù)據(jù)獲取中間層的代碼編寫效率

字符和行數(shù)優(yōu)化前后對比:

image.png

達到了既保證不破壞項目架構(gòu),又提升開發(fā)效率的目標

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市余佃,隨后出現(xiàn)的幾起案子暮刃,更是在濱河造成了極大的恐慌,老刑警劉巖爆土,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件椭懊,死亡現(xiàn)場離奇詭異,居然都是意外死亡步势,警方通過查閱死者的電腦和手機氧猬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坏瘩,“玉大人盅抚,你說我怎么就攤上這事【蠓” “怎么了妄均?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長破讨。 經(jīng)常有香客問我丛晦,道長,這世上最難降的妖魔是什么提陶? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任烫沙,我火速辦了婚禮,結(jié)果婚禮上隙笆,老公的妹妹穿的比我還像新娘锌蓄。我一直安慰自己,他們只是感情好撑柔,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布瘸爽。 她就那樣靜靜地躺著,像睡著了一般铅忿。 火紅的嫁衣襯著肌膚如雪剪决。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天檀训,我揣著相機與錄音柑潦,去河邊找鬼。 笑死峻凫,一個胖子當著我的面吹牛渗鬼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播荧琼,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼譬胎,長吁一口氣:“原來是場噩夢啊……” “哼差牛!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起堰乔,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤偏化,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后镐侯,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體夹孔,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年析孽,在試婚紗的時候發(fā)現(xiàn)自己被綠了搭伤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡袜瞬,死狀恐怖怜俐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情邓尤,我是刑警寧澤拍鲤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站汞扎,受9級特大地震影響季稳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜澈魄,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一景鼠、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧痹扇,春花似錦铛漓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至结笨,卻和暖如春包晰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背炕吸。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工伐憾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人算途。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓塞耕,卻偏偏與公主長得像蚀腿,于是被迫代替她去往敵國和親嘴瓤。 傳聞我的和親對象是個殘疾皇子扫外,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容