妙用注解

[toc]

妙用注解

前言

一、 为什么要使用注解开发

在日常开发过程中,我们经常会面临以下挑战:大量重复的模板代码、臃肿的代码降低可读性,以及JSON序列化和反序列化问题。为应对这些问题,各种编程语言都有自己的解决方案。在Dart语言中,采用了注解这一高效手段来解决这些问题。

注解就像是一把神奇的钥匙,为我们解锁了许多高效开发的大门。以下是使用注解的一些原因:

  1. 自动生成代码:注解就像会魔法的小精灵,能够在编译时自动生成代码,减轻了我们手动编写重复代码的负担,让开发变得更加轻松。
  2. 简化代码:注解可以减少重复代码,提高代码质量。
  3. 提高可读性:注解可以将元数据与实际代码分离,使代码更易于理解。
  4. 扩展功能:通过注解和注解处理器,我们可以在编译时执行一些额外操作,例如代码检查、代码优化等

二、什么是注解?

注解(Annotation)是一种编程语言特性,它不属于特定的架构或框架,而是可以在多种编程语言和开发框架中使用的一种工具。注解提供了一种方式,允许开发者在代码中添加元数据,这些元数据可以由编译器、运行时环境或其他工具在不同阶段使用。

在Dart语言中,注解是一种特殊的语法,用于在编译时或运行时向代码添加额外的元数据。注解以@符号开头,后跟一个编译时常量表达式。这种元数据可以用于指导工具(如静态分析器、编译器和构建器)执行特定操作,例如代码生成、静态检查和优化。注解不会影响程序的执行过程,但可以在编译时或运行时被工具和库访问,以实现各种目的。

三、注解实践:使用注解为每个页面类添加页面描述信息,埋点中经常需要

1、iOS:使用宏定义实现注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义宏,用于创建页面信息注解
#define CreatePageInfoAnnotation(pageClass, pageKey, pageDes) \
@interface pageClass (PageInfo) \
@property (nonatomic, readonly) NSString *pageKey; \
@property (nonatomic, readonly) NSString *pageDescription; \
@end \
@implementation pageClass (PageInfo) \
- (NSString *)pageKey { \
return pageKey; \
} \
- (NSString *)pageDescription { \
return pageDes; \
} \
@end


// LoginViewController.m
@PageInfoAnnotation(LoginViewController, @"login_page", @"登录页面") // 调用宏,为类添加注解
@implementation LoginViewController
@end

2、Flutter:使用source_gen库生成文件实现注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// page_info_generator.dart
import 'package:source_gen/source_gen.dart';
class TestGenerator extends GeneratorForAnnotation<CJPageInfoMetadata> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
String className = element.name;
String? pageKey = annotation.read("pageKey");
String? pageDesc = annotation.read("pageDesc");
return """
extension ${className}APTExtension on ${className} {
String? apt_pageKey() {
return ${pageKey};
}
String? apt_pageDesc() {
return ${pageDesc};
}
}
""";
}
}

// TestModel.dart
@CJPageInfoMetadata("我是页面描述", 'home_page')
class TestModel {
// 属性
int age;
int bookNum;

// 方法
void fun1() {}
void fun2(int a) {}
}

结果就会生成一堆dart文件。

四、进入页面前的登录判断

其他参考文章:自定义iOS注解

1、最差做法

1
2
3
4
5
6
7
8
9
10
11
12
// HomeViewController.m
- (void)goMineHomePage {
// 进入个人主页前,需要先判断是否登录
if (!UserManager.isLogin) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
return;
}

MineHomeViewController *viewController = [[MineHomeViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
}

2、统一到路由中优化:在路由中拦截+拦截器(可选)

所有跳转使用 RouterManger,在RouterManager中限制未登录允许的页面。

2.1、常规路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// HomeViewController.m
- (void)goMineHomePage {
[RouterManger push:RouterNames.mine_home_page argument:nil];
}

// RouterManger.m
- (void)push:(String *)pageName argument:(NSDictionary *)argument {
if (!UserManager.isLogin) { // 进入个人主页前,需要先判断是否登录
if ([RouterNames.mine_home_page].contains(pageName)) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[topVC pushViewController:viewController animated:YES];
return;
}
}

getVCHandle = [HandleManager.handleMap objectForKey:pageName];
UIViewController *viewController = getVCHandle(argument:argument);
[topVC pushViewController:viewController animated:YES];
}

2.2、优化路由,引进拦截器:对路由中的跳转进行改进,使用拦截器

改进后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// HomeViewController.m
- (void)goMineHomePage {
[RouterManger push:RouterNames.mine_home_page argument:nil];
}

// RouterManger.m 优化为拦截器
- (void)push:(String *)pageName argument:(NSDictionary *)argument {
for (var interceptor in interceptors) {
bool canPush = [interceptor canPushPageName:pageName arguments: arguments];
if (!canPush) return;
}

getVCHandle = [HandleManager.handleMap objectForKey:pageName];
UIViewController *viewController = getVCHandle(argument:argument);
[topVC pushViewController:viewController animated:YES];
}

// LoginInterceptor.m
- (BOOL)canPushPageName:(NSString *)pageName arguments:(NSDictionary *)arguments {
if (!UserManager.isLogin) { // 进入个人主页前,需要先判断是否登录
if ([RouterNames.mine_home_page].contains(pageName)) {
LoginViewController *viewController = [[MineHomeViewController alloc] init];
[topVC pushViewController:viewController animated:YES];
return NO;
}
}
return YES;
}

3、在调用原始方法之前插入额外的执行逻辑(不推荐,不使用)

《在调用原始方法之前插入额外的执行逻辑》

三、常见的注解示例

以下内容摘自:Flutter 注解开发

  1. @override:这个注解表示一个方法覆盖了父类的方法。它可以帮助我们检查是否正确地实现了方法覆盖,如果没有正确实现,编译器会给出警告。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Animal {
    String makeSound() {
    return "makeSound";
    }
    }

    class Dog extends Animal {
    @override
    void makeSound() {
    print('makeSound');
    }
    }
  2. @JsonSerializable():这个注解就是我们项目中使用到的json_annotation库的@JsonSerializable()注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @JsonSerializable()
    class ConversationListEntity {
    String? nextSeq;
    late List<ConversationListResList> resList;

    ConversationListEntity();

    factory ConversationListEntity.fromJson(Map<String, dynamic> json) => $ConversationListEntityFromJson(json);

    Map<String, dynamic> toJson() => $ConversationListEntityToJson(this);

    @override
    String toString() {
    return jsonEncode(this);
    }
    }
  3. 我们项目中的@RootView(jumpId: "1005"):用来为订单详情页面生成路由代码。

    1
    2
    3
    4
    5
    6
    7
    @RootView(jumpId: "1005")
    class OrderDetail extends BaseWidget {
    OrderDetail({Key? key}) : super();

    @override
    OrderDetailState createState() => OrderDetailState();
    }

附1、iOS注解原理介绍

1、在oc中实现注解一样的东西肯定是通过宏来实现的。

2、在oc中实现注解主要有4个方向:代码中注解,类注解,属性注解,方法注解。

其中代码中注解的形式应该是最常见的。你说没见过?libextobjc中那一套@weakify和@strongify你们总用得很爽吧。拆开看看这就是做代码中注解的标准形式了。而类注解,考虑了很久暂时还是误解状态,因此也没法讲了。因此实现属性注解和方法注解是关键。

3、既然是宏实现的,因此注解宏展开后应该是实际能够在对应的段落实际有效的语法才对。另外为了和面向对象的oc类型进行关联,因此在oc中可以随便乱写的c代码当然很难办了。因此我们可以做的宏很快就限定下来了,在属性中宏展开新的属性,在方法中宏展开新的方法。

在oc中实现注解一样的东西

实现

首先实现一下方法注解。由于我们知道我们需要展开方法,因此我们很快就能写出这样的宏:

1
2
3
4
5
6
7
8
// __COUNTER__ 这个宏每次使用都会自动+1
// ## 是宏的直接串接
#define path(x) \
- (id)__klmurl_path_##__COUNTER__() { \
return x ;\
}

所以结果是 __klmurl_path_1

@是哪儿来的,毕竟宏里面本来是不允许有这样的符号的。原来强项在展开的内容前加了无用的带@的表达式:参考 @weakify 其也只是宏定义weakify

1
2
3
4
5
6
#define weakify(...) \
rac_keywordify \
metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)

#define rac_keywordify autoreleasepool {}
#define rac_keywordify try {} @catch (...) {}

这样才能很自然的串一个@。

优化考虑有什么带@的而且没有什么卵用的标示呢。@compatibility_alias

这一段展开后的代码是:

1
2
3
4
5
6
7
8
9
// __COUNTER__ 这个宏每次使用都会自动+1
// ## 是宏的直接串接
#define path(x) \
compatibility_alias _KLMURL_0 KLMURL;
- (id)__klmurl_path_##__COUNTER__() { \
return x ;\
}

所以结果是 __klmurl_path_1

附1、Flutter 注解

参考文章:Flutter 注解处理及代码生成

1、注解的调用和执行结果

对 TestModel 类进行注解

1
2
3
4
5
6
7
8
9
10
@CJPageInfoMetadata("我是页面描述", 'home_page')
class TestModel {
// 属性
int age;
int bookNum;

// 方法
void fun1() {}
void fun2(int a) {}
}

以要生成如下 TestModel.g.dart 文件为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// GENERATED CODE - DO NOT MODIFY BY HAND

// **************************************************************************
// CJPageInfoMetadata
// **************************************************************************

extension TestModelAPTExtension on TestModel {
String? apt_pageKey() {
return "home_page";
}
String? apt_pageDesc() {
return "我是页面描述";
}
}

2、注解内部的实现

2.1、注解及生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 注解
class CJPageInfoMetadata {
final String pageDesc;
final String pageKey;

const CJPageInfoMetadata(this.pageDesc, this.pageKey);
}

// 生成器
import 'package:source_gen/source_gen.dart';
class TestGenerator extends GeneratorForAnnotation<CJPageInfoMetadata> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
String className = element.name;
String? pageKey = annotation.read("pageKey");
String? pageDesc = annotation.read("pageDesc");
return """
extension ${className}APTExtension on ${className} {
String? apt_pageKey() {
return ${pageKey};
}
String? apt_pageDesc() {
return ${pageDesc};
}
}
""";
}
}

2.2、使用注解生成代码的运行结果

命令执行成功后将会生成一个新的文件:TestModel.g.dart 其内容:

最后生成的文件为

2.2、生成器中 Element element, ConstantReader annotation, BuildStep buildStep 各值

2.2.1、Element element:要注解的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
element.toString: class TestModel
element.name: TestModel // 要注解的对象的【名】:TestModel
element.metadata: [@CJPageInfoMetadata("我是页面描述", 'home_page')]
element.kind: CLASS // 元素类型有 CLASSFIELDFUNCTION 等,
element.displayName: TestModel
element.fields: [int age, int bookNum] // 要注解的对象的【属性】:[int age, int bookNum]
element.methods: [fun1() → void, fun2(int a) → void] // 要注解的对象的【方法名】:[fun1() → void, ...]
element.documentationComment: null
element.enclosingElement: flutter_annotation|lib/demo_class.dart
element.hasAlwaysThrows: false
element.hasDeprecated: false
element.hasFactory: false
element.hasIsTest: false
element.hasLiteral: false
element.hasOverride: false
element.hasProtected: false
element.hasRequired: false
element.isPrivate: false
element.isPublic: true
element.isSynthetic: false
element.nameLength: 9
element.runtimeType: ClassElementImpl
...
2.2.2、ConstantReader annotation:对注解对象添加的注解信息
1
2
3
4
annotation.runtimeType: _DartObjectConstant
annotation.read("pageDesc"): '我是页面描述'
annotation.read("pageKey"): 'home_page'
annotation.objectValue: CJPageInfoMetadata (pageDesc = String ('我是页面描述'); pageKey = String ('home_page'))
2.2.3、BuildStep buildStep :提供的是该次构建的输入输出信息:
1
2
3
4
5
6
7
8
9
buildStep.runtimeType: BuildStepImpl
buildStep.inputId.path: lib/demo_class.dart
buildStep.inputId.extension: .dart
buildStep.inputId.package: flutter_annotation
buildStep.inputId.uri: package:flutter_annotation/demo_class.dart
buildStep.inputId.pathSegments: [lib, demo_class.dart]
buildStep.expectedOutputs.path: lib/demo_class.g.dart
buildStep.expectedOutputs.extension: .dart
buildStep.expectedOutputs.package: flutter_annotation

3、使用注解生成代码前的准备

注解的生成器 Generator 的执行需要 Builder 来触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1、使用Generator 创建一个Builder
Builder testBuilder(BuilderOptions options) => LibraryBuilder(TestGenerator());

// 2、创建 build.yaml 文件,将我们要执行的 builder 配置进去
builders:
testBuilder:
import: "package:flutter_annotation/test.dart"
builder_factories: ["testBuilder"]
build_extensions: {".dart": [".g.part"]}
auto_apply: root_package
build_to: source

// 3、运行 Builder,命令执行成功后将会生成一个新的文件:TestModel.g.dart 其内容:
flutter packages pub run build_runner build

Flutter注解的Builder的原理很像使用shell或者python脚本遍历文件的方式。

已知@Description是注解,其作用为提取 @Description 的方法描述到一个文件。

请说说@Description注解的内部实现(使用shell或者python脚本遍历文件的方式,不在本次讨论中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// my_class.dart
class MyClass {
@Description('This method does something important.') // 注解
void myImportantMethod() {
print('Doing something important.');
}
}

// description_generator.dart
import 'package:source_gen/source_gen.dart';
import 'package:code_builder/code_builder.dart';
import 'package:annotations/annotations.dart'; // 引入上面创建的注解库

class DescriptionGenerator extends GeneratorForAnnotation<Description> {
@override
String? generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) { {
// 检查注解是否应用在方法上
if (element.isMethod) {
final description = annotation.read('value').stringValue;
final method = element.asMethod();

// 创建一个方法调用的描述文本
final code = Block.of([
Directive.code("print('Method ${method.name} description: $description');")
]);

// 返回生成的代码
return code.toString();
}

// 如果注解没有应用在方法上,返回空字符串
return '';
}
}

其他参考文章

https://pub.dev/packages/mustache_template

mustache_template 是一个用于Flutter的Dart模板库,它支持dart2jsdart2native

End