Flutter從0開發一個路由框架

本文教你使用source_gen來開發本身的路由框架了,在開發的時候能夠考慮某些功能用此方式開發android

source_gen是封裝自build和 analyzer,並在此基礎上提供友好的api封裝。build是一個提供構建控制的庫,analyzer是提供dart語法靜態分析功能的庫,source_gen將其整合即可以實現一套基於註解的代碼生成工具。 ios

本文的教程順序以下:git

  1. 建立項目,添加代碼生成庫
  2. 建立註解類
  3. mustache4dart的使用,建立代碼模板
  4. generator文件的建立,該文件用來生成代碼
  5. builder文件的建立,該文件用來使用generator文件生成指定的dart文件
  6. build.yaml文件的建立和字段說明

就這麼簡單,下面咱們逐個講解github

1. 建立項目

首先,咱們先建立一個Flutter項目,或者一個純Dart項目,這裏咱們建立一個純Dart項目,純Dart項目是不包含Android和IOS平臺代碼的,這裏咱們用不到平臺的東西,因此建立純Dart項目便可api

點擊Finish,就完成了純Dart項目的建立(先無視demo,由於我所有建立完之後才截的圖)bash

由於接下來咱們要用到source_genmustache4dart這兩個庫,因此咱們將這兩個庫加到根目錄的pubspec.yaml裏的dependencies下面,若是隻在這個項目裏用不提供給其餘項目,能夠加到dev_dependenciesmarkdown

dependencies:
 flutter:
 sdk: flutter
 source_gen:
 mustache4dart:
複製代碼

2. 建立註解類

建立文件core.dart, 咱們把咱們所須要的註解類和輔助類都放到這裏,方便管理。那麼都須要什麼註解呢,若是跳轉,咱們須要知道要跳轉的頁面的路徑、可能攜帶的參數、跳轉成功的widget。app

注意: 註解必須有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生成的類,路由器類不是必須的,可是咱們必須隨便註釋一個類來生成實際的路由跳轉代碼。less

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); } """;
複製代碼

這個文件裏的importsrouteMaprouteMap一會都會被替換成實際代碼,編寫這個文件的時候,除了佔位代碼,其餘能夠像平時同樣來寫,最後在最前面和最後面加上"""便可。

4. generator文件的建立,該文件用來生成代碼

首先咱們須要根據註解,將url和對應的Widget保存到一個map集合裏,還須要一個集合用來保存import的內容,而後再將這些變量替換代碼模板裏的佔位符,而後生成代碼便可,咱們分如下幾步

1 將url和對應的Widget保存到名爲routeMap的集合裏
2 將Widget所在文件保存到名爲imports的集合裏
3 將routeMap和imports替換代碼模板裏的佔位符
複製代碼

第一、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: ../
複製代碼

至於dependenciesdev_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就完成了,運行一下,就看到效果了

如何在項目中使用

項目已發佈到githubpub

該項目開發中須要注意的地方:

咱們的core.dart文件裏不能import 'package:flutter/widgets.dart',不然生成代碼的時候會報錯


參考文章:

source_gen官方文檔

build_config官方文檔

mustache4dart官方文檔

常見的開源協議

相關文章
相關標籤/搜索