Flutter系列:4.基於註解的程式碼生成應用
前言
api資料序列化為model例項是移動開發中很常見也是很基礎的技術點,得益於執行時等動態技術在ios開發中我們可以藉助JSONModel或者SwiftyJSON很方便的實現序列化,對於剛剛接觸flutter的開發者來說其序列化體驗無疑是非常糟糕的。本身Dart語言是支援反射的,但是在Flutter中,Dart幾乎放棄了指令碼語言動態化的特性,如不支援反射、也不支援動態建立函式等;所以序列化只有依靠攔截註解來動態生成程式碼的方式實現。
註解
註解是一種可以為程式碼提供一些語義資訊或元資料的標註,這在其他語言中也很常見,在dart中常見的註解有@deprecated、@override等,註解是以@開頭的,他們可以作用於類,函式,屬性等。
dart中自定義註解很簡單,其實現就是一個帶有const建構函式的類
library todo; class Todo { final String who; final String what; const Todo(this.who, this.what); }
然後就可以這樣使用Todo這個註解了
import 'todo.dart'; @Todo('seth', 'make this do something') void doSomething() { print('do something'); }
source_gen
通過註解的方式我們就可以為類或者屬性新增一個額外的資料資訊, source_gen 可以攔截註解獲取並解析上下文資訊,通過解析註解實現source_gen的相關Generator就可以動態的生成程式碼了;
source_gen是封裝自build和 analyzer,並在此基礎上提供友好的api封裝。build是一個提供構建控制的庫,analyzer是提供dart語法靜態分析功能的庫,source_gen將其整合便可以實現一套基於註解的程式碼生成工具。
程式碼生成
使用Annotation+source_gen的方式可以便捷的生成程式碼,source_gen通過攔截Annotation,解析其上下文element然後通過builder即可動態生成程式碼,下面簡易的程式碼生成Demo。
建立package
終端執行:
flutter create --template=package code_gen_demo
vscode開啟剛剛建立的package, pubspec.yaml新增source_gen和build_runner依賴
dependencies: flutter: sdk: flutter source_gen: '>=0.8.0'
lib目錄下建立註解mark.dart
class Mark { final String name; const Mark({this.name}); }
建立程式碼生成器generator.dart 負責攔截我們的註解Mark, 解析註解的類名稱,路徑及其引數name並返回
import 'package:analyzer/dart/element/element.dart'; import 'package:source_gen/source_gen.dart'; import 'package:build/build.dart'; import 'mark.dart'; class MarkGenerator extends GeneratorForAnnotation<Mark> { @override generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { String className = element.displayName; String path = buildStep.inputId.path; String name =annotation.peek('name').stringValue; return "//$className\n//$path\n//$name"; } }
lib目錄建立構建器builder.dart, 新增一個頂級方法markBuilder供build runner解析呼叫
import 'package:source_gen/source_gen.dart'; import 'package:build/build.dart'; import 'mark_generator.dart'; Builder markBuilder(BuilderOptions options) => LibraryBuilder(MarkGenerator(), generatedExtension: '.mark.dart');
在package根目錄下新增build.yaml檔案(buildRunner會解析其配置執行builder指定的方法),配置成剛剛建立的builder內容如下
targets: $default: builders: code_gen_demo|mark_builder: enabled: true builders: mark_builder: import: 'package:code_gen_demo/builder.dart' builder_factories: ['markBuilder'] build_extensions: { '.dart': ['.mark.dart'] } auto_apply: root_package build_to: source
import指定了builder的位置,builder_factories指定了builder的具體呼叫,build_extensions指定了輸入輸入檔案的格式匹配,此列會生成".mark.dart"結尾的檔案。
至此程式碼生成相關的Annotation、 builder和Generator都準備好了,接下來我們建立example工程來做示例
建立example工程
在package的根目錄下建立example工程,example是一個完整的flutter工程,執行命令:
flutter create example
在example工程中引入我們的package, 在example的pubspec.yaml中新增依賴package,以及新增對builder_runner的依賴來執行編譯命令
dependencies: flutter: sdk: flutter code_gen_demo: path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.2 dev_dependencies: flutter_test: sdk: flutter build_runner: '>=0.9.1'
建立一個示例類,mark_demo.dart, 並新增Mark註解
import 'package:code_gen_demo/mark.dart'; @Mark(name: "hello") class MarkDemo { }
好了,接下來在example目錄下執行builder runner命令來為Mark註解的mark_demo.dart生成一個相關程式碼mark_demo.mark.dart
flutter packages pub run build_runner build --delete-conflicting-outputs
重新執行run builder_runner前最好先clean一下
flutter packages pub run build_runner clean
命令執行完成後就可以看到在mark_demo.dart檔案下生成了一個mark_demo.mark.dart的檔案,其內容是mark_generator.dart中為Mark這個註解建立的Generator返回的內容:
// GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** // MarkGenerator // ************************************************************************** //MarkDemo //lib/mark_demo.dart //hello
本demo原始碼位置 GitHub
easy_router
目前在Flutter中常見的程式碼生成主要應用在json序列化庫json_serializable中,在國內閒魚技術團隊使用這一技術實現了一套router的路由對映解決方案 annotation_route ,感興趣的可以看看。
作為學習我參考了閒魚的annotation_route實現了一個簡單的Flutter頁面路由匹配方案easy_router,不同於閒魚annotation_route的複雜和全面,簡單實現路由url的匹配、引數解析賦值並返回page例項。
easy_router原始碼 戳我
使用方式
使用@EasyRoute來註解需要加入Router的page, url作為page的唯一標識,例如
@EasyRoute(url: "easy://flutter/pagea") class PageA extends StatefulWidget { final EasyRouteOption routeOption; PageA(this.routeOption); @override _PageAState createState() => _PageAState(); }
easy_router會呼叫page的建構函式並傳入EasyRouteOption引數,所以每個page都應該有一個這樣的建構函式,如果url有引數,引數會放到EasyRouteOption物件的params屬性中,以便page獲取。
使用@easyRouter來註解你的router, 這樣就會生成router相關的內部邏輯, 例如
import 'package:example/route.router.internal.dart'; import 'package:easy_router/route.dart'; @easyRouter class Router { EasyRouterInternal internalImpl = EasyRouterInternalImpl(); dynamic getPage(String url) { EasyRouteResult result = internalImpl.router(url); if(result.state == EasyRouterResultState.NOT_FOUND) { print("Router error: page not found"); return null; } return result.widget; } }
EasyRouterInternalImpl就是最終生成的router實現, 執行命令生成EasyRouterInternalImpl實現
flutter packages pub run build_runner build --delete-conflicting-outputs
呼叫router開啟url對應的page
MaterialButton( child: Text('ToPageA'), onPressed: (){ Navigator.of(context).push( MaterialPageRoute( builder: (context) { return Router().getPage('easy://flutter/pagea?parama=a'); } ) ); }, ),
感興趣自己改改,詳細使用參看原始碼example
實現方式
routeParseBuilder:負責解析@EasyRoute註解的page頁面,完成page和url的對映關係
routerBuilder:讀取routeParseBuilder生成的對映,完成對EasyRouterInternalImpl寫入,依賴mustache4dart庫完成替換寫入