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将其整合便可以实现一套基于注解的代码生成工具。

Flutter系列:4.基于注解的代码生成应用

代码生成

使用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库完成替换写入

相关推荐