本文教你使用source_gen來開發自己的路由框架了,在開發的時候可以考慮某些功能用此方式開發
source_gen是封裝自build和 analyzer,並在此基礎上提供友好的api封裝。build是一個提供構建控制的庫,analyzer是提供dart語法靜態分析功能的庫,source_gen將其整合便可以實現一套基於註解的程式碼生成工具。
本文的教程順序如下:
- 建立專案,新增程式碼生成庫
- 建立註解類
- mustache4dart的使用,建立程式碼模板
- generator檔案的建立,該檔案用來生成程式碼
- builder檔案的建立,該檔案用來使用generator檔案生成指定的dart檔案
- build.yaml檔案的建立和欄位說明
就這麼簡單,下面我們逐個講解
1. 建立專案
首先,我們先建立一個Flutter專案,或者一個純Dart專案,這裡我們建立一個純Dart專案,純Dart專案是不包含Android和IOS平臺程式碼的,這裡我們用不到平臺的東西,所以建立純Dart專案即可
點選Finish,就完成了純Dart專案的建立(先無視demo,因為我全部建立完以後才截的圖)
因為接下來我們要用到source_gen
和mustache4dart
這兩個庫,所以我們將這兩個庫加到根目錄的pubspec.yaml
裡的dependencies
下面,如果只在這個專案裡用不提供給其他專案,可以加到dev_dependencies
裡
dependencies:
flutter:
sdk: flutter
source_gen:
mustache4dart:
複製程式碼
2. 建立註解類
建立檔案core.dart
, 我們把我們所需要的註解類和輔助類都放到這裡,方便管理。那麼都需要什麼註解呢,如果跳轉,我們需要知道要跳轉的頁面的路徑、可能攜帶的引數、跳轉成功的widget。
注意: 註解必須有const的建構函式
完整程式碼core.dart
/// 作者:liuhc
/// 定義頁面路由註解
/// Define page routing annotations
class EasyRoutePathAnnotation {
final String url;
final bool hasParam;
const EasyRoutePathAnnotation(this.url, this.hasParam);
}
/// EasyRoutePathAnnotation的hasParam為true的時候必須新增一個接受此引數的建構函式
/// When the hasParam of EasyRoutePathAnnotation is true, you must add a constructor that accepts this parameter.
class EasyRouteParam {
final Map<String, dynamic> params;
EasyRouteParam(this.params);
}
/// 定義路由註解
/// Define route parser annotations
class EasyRouterAnnotation {
const EasyRouterAnnotation();
}
複製程式碼
然後這步就結束了。EasyRoutePathAnnotation
是我們要新增到被跳轉頁面的註解。EasyRouteParam
這個類需要所有通過我們的路由器跳轉的頁面新增帶EasyRouteParam
引數的構造器。EasyRouterAnnotation
用來註解我們封裝的路由器類,這個路由器類呼叫我們通過source_gen
生成的類,路由器類不是必須的,但是我們必須隨便註釋一個類來生成實際的路由跳轉程式碼。
3. mustache4dart的使用,建立程式碼模板
到這裡我們就用到mustache4dart這個類庫了,我們這個框架的這一步是最重要的,用這個庫,我們可以很方便的生成程式碼,不熟悉的可以看官方文件(文章底部提供了地址),我們這裡用到的它的api並不多,我們的模板程式碼如下,這裡我給這個檔案起名叫generate_code_template.dart
/// author:liuhc
const String codeTemplate = """
import 'package:easy_router/easy_router.dart';
import 'package:flutter/widgets.dart';
{{#imports}}
import '{{{path}}}';
{{/imports}}
class EasyRouter {
static EasyRouter get instance => _getInstance();
static EasyRouter _instance;
EasyRouter._internal();
factory EasyRouter()=> _getInstance();
static EasyRouter _getInstance() {
if (_instance == null) {
_instance = EasyRouter._internal();
}
return _instance;
}
final Map<String, Pair<dynamic, bool>> _routeMap = {{{routeMap}}};
Widget getWidget(String url, {Map<String, dynamic> param}) {
try {
final Type pageClass = _routeMap[url].clazz;
if (pageClass == null) {
return null;
}
final bool hasParam = _routeMap[url].hasParam;
return _createInstance(pageClass, hasParam, param);
} catch (e) {
print(e.toString());
return null;
}
}
dynamic _createInstance(Type clazz, bool hasParam, Map<String, dynamic> param) {
{{{classInstance}}}
}
}
class Pair<E, F> {
E clazz;
F hasParam;
Pair(this.clazz, this.hasParam);
}
""";
複製程式碼
這個檔案裡的imports
、routeMap
、routeMap
一會都會被替換成實際程式碼,編寫這個檔案的時候,除了佔位程式碼,其他可以像平時一樣來寫,最後在最前面和最後面加上"""即可。
4. generator檔案的建立,該檔案用來生成程式碼
首先我們需要根據註解,將url和對應的Widget儲存到一個map集合裡,還需要一個集合用來儲存import的內容,然後再將這些變數替換程式碼模板裡的佔位符,然後生成程式碼即可,我們分以下幾步
1 將url和對應的Widget儲存到名為routeMap的集合裡
2 將Widget所在檔案儲存到名為imports的集合裡
3 將routeMap和imports替換程式碼模板裡的佔位符
複製程式碼
第1、2步驟我寫到了generator_param.dart
檔案裡,在這一步,我們並不需要生成任何檔案,這一步只需要將第3步需要的引數生成即可。
/// author:liuhc
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'core.dart';
/// This file is used to save the parameters needed to generate the code.
/// 該檔案用來儲存生成程式碼所需的引數
class EasyRoutePathGenerator extends GeneratorForAnnotation<EasyRoutePathAnnotation> {
static Map<String, Pair<dynamic, bool>> routeMap = {};
static List<String> importList = [];
static String classInstanceContent;
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
routeMap = _parseRouteMap(routeMap, element, annotation);
importList = _parseImportList(importList, buildStep);
classInstanceContent = _generateInstance(routeMap);
return null;
}
/// 1 Save the url and the corresponding Widget to a collection named routeMap
/// 1 將url和對應的Widget儲存到名為routeMap的集合裡
Map<String, dynamic> _parseRouteMap(
Map<String, Pair<dynamic, bool>> routeMap, Element element, ConstantReader annotation) {
String clazz = element.displayName;
print("parse element=$clazz");
String url = annotation.peek('url').stringValue;
bool hasParam = annotation.peek('hasParam').boolValue;
/// May be a bug in mustache4dart, if you don't add ' before and after the url, the generated code is problematic.
/// 可能是mustache4dart的bug,如果不給url前後新增'的話,生成的程式碼是有問題的
String urlKey = "'" + url + "'";
if (routeMap.containsKey(urlKey)) {
return routeMap;
}
routeMap[urlKey] = Pair(clazz, hasParam);
return routeMap;
}
/// 2 Save the file where the Widget is located to a collection named imports
/// 2 將Widget所在檔案儲存到名為imports的集合裡
List<String> _parseImportList(List<String> importList, BuildStep buildStep) {
String path = buildStep.inputId.path;
print("parse path=$path");
if (path.contains("lib/")) {
path = path.replaceFirst("lib/", "");
}
if (!importList.contains(path)) {
importList.add(path);
}
return importList;
}
/// Generate a switch statement to get different Widgets through different urls
/// 生成switch語句,通過不同的url獲取不同的Widget
String _generateInstance(Map<String, Pair<dynamic, bool>> routeMap) {
StringBuffer stringBuffer = StringBuffer();
stringBuffer.writeln("switch (clazz) {");
routeMap.forEach((String url, Pair<dynamic, bool> pair) {
if (pair.hasParam) {
stringBuffer.writeln("case ${pair.clazz} : ");
stringBuffer.writeln("EasyRouteParam easyRouteParam = EasyRouteParam(param);");
stringBuffer.writeln("return ${pair.clazz}(easyRouteParam);");
} else {
stringBuffer.writeln("case ${pair.clazz} : return ${pair.clazz}();");
}
});
stringBuffer.writeln("default: return null;}");
return stringBuffer.toString();
}
}
class Pair<E, F> {
E clazz;
F hasParam;
Pair(this.clazz, this.hasParam);
}
複製程式碼
然後第3步,生成程式碼,這裡我們給該檔案取名為generator_router.dart
/// author:liuhc
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:mustache4dart/mustache4dart.dart';
import 'package:source_gen/source_gen.dart';
import 'core.dart';
import 'generate_code_template.dart';
import 'generator_param.dart';
/// This file is used to generate EasyRouter
/// 該檔案用來生成EasyRouter
class EasyRouterGenerator extends GeneratorForAnnotation<EasyRouterAnnotation> {
@override
generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
/// 3 Replace placeholders with routemap and imports in the code template
/// 3 將routeMap和imports替換程式碼模板裡的佔位符
return render(codeTemplate, <String, dynamic>{
'imports': EasyRoutePathGenerator.importList.map((item) => {'path': item}).toList(),
'classInstance': EasyRoutePathGenerator.classInstanceContent,
'routeMap': EasyRoutePathGenerator.routeMap
.map((String key, Pair<dynamic, bool> value) => MapEntry(key, "Pair(${value.clazz},${value.hasParam})"))
.toString()
});
}
}
複製程式碼
5. builder檔案的建立,該檔案用來使用generator檔案生成指定的dart檔案
在我們執行命令生成程式碼的時候,我們會指定builder檔案的位置,然後指令碼會自動根據該檔案來生成程式碼,檔名字隨意,這裡我們就叫builder.dart
,程式碼如下
完整程式碼builder.dart
/// author:liuhc
import 'package:build/build.dart';
import 'package:easy_router/src/generator_router.dart';
import 'package:source_gen/source_gen.dart';
import 'generator_param.dart';
/// Does not generate files here
/// 這裡並不生成檔案
Builder paramBuilder(BuilderOptions options) =>
LibraryBuilder(EasyRoutePathGenerator(), generatedExtension: ".empty.dart");
/// 生成".g.dart"結尾的檔案
/// Generate a file ending with ".g.dart"
Builder routerBuilder(BuilderOptions options) =>
LibraryBuilder(EasyRouterGenerator(), generatedExtension: ".g.dart");
複製程式碼
6. build.yaml檔案的建立和欄位說明
在專案的根目錄下建立build.yaml
檔案,檔名字必須是這個,檔案內容如下
完整程式碼build.yaml
# Read about `build.yaml` at https://pub.flutter-io.cn/packages/build_config
# import指定了builder的位置,
# builder_factories指定了builder的具體呼叫,
# build_extensions指定了輸入輸入檔案的格式匹配,
builders:
param_builder:
import: 'package:easy_router/src/builder.dart'
builder_factories: ['paramBuilder']
build_extensions: { '.dart': ['.g.dart'] }
auto_apply: root_package
build_to: source
router_builder:
import: 'package:easy_router/src/builder.dart'
builder_factories: ['routerBuilder']
build_extensions: { '.dart': ['.g.dart'] }
auto_apply: root_package
build_to: source
複製程式碼
路由器框架完成
到這裡我們的路由框架就完成了
編寫測試demo
我們寫個demo來測試一下,在當前專案里根目錄執行命令flutter create demo
,這個demo專案會包含android和ios平臺。
注意,這裡用命令列來建立demo,如果使用as建立的話,as不會把demo建立到當前專案裡
因為我們需要使用剛才寫好的路由庫,還需要source_gen
生成程式碼,所以修改demo下面的pubspec.yaml
檔案,在dev_dependencies
下面新增如下程式碼
dev_dependencies:
build_runner:
easy_router:
path: ../
複製程式碼
至於
dependencies
和dev_dependencies
的區別不是本文的重點,欲知詳情自己谷歌
然後我們在我們的demo下面新增測試程式碼
main.dart
/// description: easy_router demo app
/// author: liuhc
import 'package:flutter/material.dart';
import 'router.dart';
void main() {
runApp(
MaterialApp(
title: '簡單路由',
home: MainPage(),
theme: ThemeData(
primarySwatch: Colors.blue,
),
),
);
}
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("首頁"),
),
body: ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
_getButton(context, "pageA", "跳轉到頁面A", param: {"key": "a"}),
_getButton(context, "pageB", "跳轉到頁面B"),
_getButton(context, "pageC", "跳轉到不存在的頁面"),
],
),
),
);
}
Widget _getButton(
BuildContext context,
String url,
String text, {
Map<String, dynamic> param,
}) {
return RaisedButton(
onPressed: () {
Router.instance.go(context, url, param: param);
},
child: Text(text),
);
}
}
複製程式碼
page_a.dart
/// description: test page A
/// author: liuhc
import 'package:flutter/material.dart';
import 'package:easy_router/easy_router.dart';
@EasyRoutePathAnnotation("pageA", true)
class PageA extends StatelessWidget {
final EasyRouteParam param;
PageA(this.param);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Page A"),
),
body: Container(
alignment: Alignment.center,
child: Text("param:${param["key"]}"),
),
);
}
}
複製程式碼
page_b.dart
/// description: test page B
/// author: liuhc
import 'package:flutter/material.dart';
import 'package:easy_router/easy_router.dart';
@EasyRoutePathAnnotation("pageB", false)
class PageB extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Page B"),
),
body: Container(
alignment: Alignment.center,
child: Text("no param"),
),
);
}
}
複製程式碼
router.dart
/// 作者:liuhc
import 'package:easy_router/easy_router.dart' show EasyRouterAnnotation;
import 'package:flutter/material.dart';
@EasyRouterAnnotation()
class Router {
static Router get instance => _getInstance();
static Router _instance;
Router._internal();
factory Router() => _getInstance();
static Router _getInstance() {
if (_instance == null) {
_instance = Router._internal();
}
return _instance;
}
Widget getWidget(String url, {Map<String, dynamic> param}) {
//TODO
}
void go(BuildContext context, String url, {Map<String, dynamic> param}) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) {
return getWidget(url, param: param);
},
),
);
}
}
class NotFoundPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("404"),
),
body: Container(
alignment: Alignment.center,
child: Text("沒有找到頁面"),
),
);
}
}
複製程式碼
執行命令生成程式碼
然後在demo目錄下執行命令生成程式碼
flutter packages pub run build_runner build --delete-conflicting-outputs
複製程式碼
推薦生成程式碼之前先清除以前的程式碼
flutter packages pub run build_runner clean
複製程式碼
然後就生成了router.g.dart
檔案
然後我們修改剛才的router.dart
檔案,新增import 'router.g.dart';
,修改getWidget
方法
Widget getWidget(String url, {Map<String, dynamic> param}) {
return EasyRouter.instance.getWidget(url, param: param) ?? NotFoundPage();
}
複製程式碼
完成demo
到這裡,我們的demo就完成了,執行一下,就看到效果了
如何在專案中使用
該專案開發中需要注意的地方:
我們的core.dart
檔案裡不能import 'package:flutter/widgets.dart'
,否則生成程式碼的時候會報錯
參考文章: