Flutter数据分析插件candies_analyzer_plugin的使用

Flutter数据分析插件candies_analyzer_plugin的使用

描述

该插件用于帮助快速创建自定义的分析工具。


简单使用

pubspec.yaml中添加依赖
dev_dependencies:
  # zmtzawqlp  
  candies_analyzer_plugin: any
analysis_options.yaml中启用插件
analyzer:
  # zmtzawqlp  
  plugins:
    candies_analyzer_plugin

默认的分析项包括:

  • prefer_asset_const
  • prefer_named_routes
  • prefer_safe_setState
  • must_call_super_dispose
  • perfer_doc_comments
  • prefer_singleton
  • good_doc_comments
  • prefer_trailing_comma

更多详情请参见默认分析项


自定义你的分析插件

创建模板
  1. 激活插件

运行以下命令来激活插件:

dart pub global activate candies_analyzer_plugin
  1. 进入项目目录

假设你的项目名为example,你的分析插件名为custom_lint。运行以下命令生成简单的分析插件:

candies_analyzer_plugin --example custom_lint
  1. 在项目的根目录pubspec.yaml中添加custom_lintdev_dependencies
dev_dependencies:
  # zmtzawqlp  
  custom_lint:
    path: custom_lint/
  1. 在项目的根目录analysis_options.yaml中添加custom_lintanalyzer plugins
analyzer:
  # zmtzawqlp  
  plugins:
    custom_lint

完成分析后,你将在IDE中看到一些自定义的分析项。


添加你的分析项

找到基于以下项目结构的plugin.dart文件:

├─ example
│  ├─ custom_lint
│  │  └─ tools
│  │     └─ analyzer_plugin
│  │        ├─ bin
│  │        │  └─ plugin.dart

plugin.dart是插件的入口点。

启动插件

在该文件中启动插件:

CandiesAnalyzerPlugin get plugin => CustomLintPlugin();

// This file must be 'plugin.dart'
void main(List<String> args, SendPort sendPort) {
  CandiesAnalyzerPluginStarter.start(
    args,
    sendPort,
    plugin: plugin,
  );
}

class CustomLintPlugin extends CandiesAnalyzerPlugin {
  [@override](/user/override)
  String get name => 'custom_lint';

  [@override](/user/override)
  List<String> get fileGlobsToAnalyze => const [
        '**/*.dart',
        '**/*.yaml',
        '**/*.json',
      ];

  [@override](/user/override)
  List<DartLint> get dartLints => [
        // 添加你的Dart分析项
        PerferCandiesClassPrefix(),
        ...super.dartLints,
      ];

  [@override](/user/override)
  List<YamlLint> get yamlLints => [
        RemoveDependency(package: 'path'),
      ];

  [@override](/user/override)
  List<GenericLint> get genericLints => [
        RemoveDuplicateValue(),
      ];
}
创建一个分析项

你只需要创建一个继承自DartLintYamlLintGenericLint的自定义分析项。

属性:

属性名 描述 默认值
code 错误代码的名称 必填
message 显示的消息 必填
url 错误相关的文档页面的URL 可选
type 错误类型 默认为LINT
severity 错误的严重性 默认为INFO
correction 修复消息 可选
contextMessages 提供上下文信息的附加消息 可选

重要方法:

方法名 描述 是否覆盖
matchLint 返回是否匹配分析项 必须覆盖
getDartFixes/getYamlFixes/getGenericFixes 返回修复项 不支持Yaml和Generic的修复项
Dart分析项

你可以通过覆盖ignoreLintignoreFile来忽略分析项或忽略文件。以下是一个Dart分析项的示例:

class PerferCandiesClassPrefix extends DartLint {
  [@override](/user/override)
  String get code => 'perfer_candies_class_prefix';

  [@override](/user/override)
  String? get url => 'https://github.com/fluttercandies/candies_analyzer_plugin';

  [@override](/user/override)
  SyntacticEntity? matchLint(AstNode node) {
    if (node is ClassDeclaration) {
      final String name = node.name2.toString();
      final int startIndex = _getClassNameStartIndex(name);
      if (!name.substring(startIndex).startsWith('Candies')) {
        return node.name2;
      }
    }
    return null;
  }

  [@override](/user/override)
  String get message => '定义类名以Candies开头';

  [@override](/user/override)
  Future<List<SourceChange>> getDartFixes(
    DartAnalysisError error,
    CandiesAnalyzerPluginConfig config,
  ) async {
    final ResolvedUnitResult resolvedUnitResult = error.result;
    final AstNode astNode = error.astNode;
    final Token nameNode = (astNode as ClassDeclaration).name2;
    final String nameString = nameNode.toString();
    return [
      await getDartFix(
        resolvedUnitResult: resolvedUnitResult,
        message: '使用Candies作为类前缀。',
        buildDartFileEdit: (DartFileEditBuilder dartFileEditBuilder) {
          final int startIndex = _getClassNameStartIndex(nameString);
          final RegExp regExp = RegExp(nameString);
          final String replace = '${nameString.substring(0, startIndex)}Candies${nameString.substring(startIndex)}';
          for (final Match match in regExp.allMatches(resolvedUnitResult.content)) {
            dartFileEditBuilder.addSimpleReplacement(
              SourceRange(match.start, match.end - match.start),
              replace,
            );
          }
          dartFileEditBuilder.formatAll(resolvedUnitResult.unit);
        },
      )
    ];
  }

  int _getClassNameStartIndex(String nameString) {
    int index = 0;
    while (nameString[index] == '_') {
      index++;
      if (index == nameString.length - 1) {
        break;
      }
    }
    return index;
  }
}
Yaml分析项

以下是一个Yaml分析项的示例:

class RemoveDependency extends YamlLint {
  RemoveDependency({required this.package});
  final String package;
  [@override](/user/override)
  String get code => 'remove_${package}_dependency';

  [@override](/user/override)
  String get message => '不要使用$package!';

  [@override](/user/override)
  String? get correction => '移除$package依赖';

  [@override](/user/override)
  AnalysisErrorSeverity get severity => AnalysisErrorSeverity.WARNING;

  [@override](/user/override)
  Iterable<SourceRange> matchLint(
    YamlNode root,
    String content,
    LineInfo lineInfo,
  ) sync* {
    if (root is YamlMap && root.containsKey(PubspecField.DEPENDENCIES_FIELD)) {
      final YamlNode dependencies = root.nodes[PubspecField.DEPENDENCIES_FIELD]!;
      if (dependencies is YamlMap && dependencies.containsKey(package)) {
        final YamlNode get = dependencies.nodes[package]!;
        int start = dependencies.span.start.offset;
        final int end = get.span.start.offset;
        final int index = content.substring(start, end).indexOf('$package: ');
        start += index;
        yield SourceRange(start, get.span.end.offset - start);
      }
    }
  }
}
通用分析项

以下是一个通用分析项的示例:

class RemoveDuplicateValue extends GenericLint {
  [@override](/user/override)
  String get code => 'remove_duplicate_value';

  [@override](/user/override)
  Iterable<SourceRange> matchLint(
    String content,
    String file,
    LineInfo lineInfo,
  ) sync* {
    if (isFileType(file: file, type: '.json')) {
      final Map<dynamic, dynamic> map = jsonDecode(content) as Map<dynamic, dynamic>;

      final Map<dynamic, dynamic> duplicate = {};
      final Map<dynamic, dynamic> checkDuplicate = {};
      for (final dynamic key in map.keys) {
        final dynamic value = map[key];
        if (checkDuplicate.containsKey(value)) {
          duplicate[key] = value;
          duplicate[checkDuplicate[value]] = value;
        }
        checkDuplicate[value] = key;
      }

      if (duplicate.isNotEmpty) {
        for (final dynamic key in duplicate.keys) {
          final int start = content.indexOf('"$key"');
          final dynamic value = duplicate[key];
          final int end = content.indexOf(
                '"$value"',
                start,
              ) +
              value.toString().length +
              1;

          final int lineNumber = lineInfo.getLocation(end).lineNumber;

          bool hasComma = false;
          int commaIndex = end;
          int commaLineNumber = lineInfo.getLocation(commaIndex).lineNumber;

          while (!hasComma && commaLineNumber == lineNumber) {
            commaIndex++;
            final String char = content[commaIndex];
            hasComma = char == ',';
            commaLineNumber = lineInfo.getLocation(commaIndex).lineNumber;
          }

          yield SourceRange(start, (hasComma ? commaIndex : end) + 1 - start);
        }
      }
    }
  }

  [@override](/user/override)
  String get message => '删除重复值';
}

调试

调试分析项

找到基于以下项目结构的debug.dart文件:

├─ example
│  ├─ custom_lint
│  │  └─ tools
│  │     └─ analyzer_plugin
│  │        ├─ bin
│  │        │  └─ debug.dart

更改根目录为你想要调试的目录,默认是example文件夹。

import 'dart:io';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart';
import 'package:candies_analyzer_plugin/candies_analyzer_plugin.dart';
import 'plugin.dart';

Future<void> main(List<String> args) async {
  final String root = Directory.current.parent.parent.parent.path;
  final AnalysisContextCollection collection = AnalysisContextCollection(includedPaths: [root]);

  final CandiesAnalyzerPlugin myPlugin = plugin;
  for (final AnalysisContext context in collection.contexts) {
    for (final String file in context.contextRoot.analyzedFiles()) {
      if (!myPlugin.shouldAnalyzeFile(file, context)) {
        continue;
      }

      final bool isAnalyzed = context.contextRoot.isAnalyzed(file);
      if (!isAnalyzed) {
        continue;
      }

      final List<AnalysisError> errors = (await myPlugin.getAnalysisErrorsForDebug(
        file,
        context,
      )).toList();
      for (final AnalysisError error in errors) {
        final List<AnalysisErrorFixes> fixes = await myPlugin
            .getAnalysisErrorFixesForDebug(
                EditGetFixesParams(file, error.location.offset), context)
            .toList();
        print(fixes.length);
      }

      print(errors.length);
    }
  }
}

更新代码

有两种方式更新新的代码到dartServer

  1. 删除.plugin_manager文件夹

注意,analyzer_plugin文件夹会被复制到.plugin_manager并根据加密插件路径创建一个文件夹。

macOS: /Users/user_name/.dartServer/.plugin_manager/ Windows: C:\Users\user_name\AppData\Local\.dartServer\.plugin_manager\

如果代码有变化,请删除plugin_manager下的文件。

或者可以运行candies_analyzer_plugin --clear-cache来删除plugin_manager下的文件。

  1. custom_lint文件夹下写新的代码

你可以在custom_lint下编写新的代码,例如在custom_lint.dart中。

├─ example
│  ├─ custom_lint
│  │  ├─ lib
│  │  │  └─ custom_lint.dart

所以你应该在analyzer_plugin\pubspec.yaml中添加custom_lint依赖。

由于analyzer_plugin文件夹会被复制到.plugin_manager,你应该使用绝对路径。

如果你不打算发布custom_lint作为新包,我不建议这样做。

├─ example
│  ├─ custom_lint
│  │  ├─ lib
│  │  │  └─ custom_lint.dart
│  │  └─ tools
│  │     └─ analyzer_plugin
│  │        ├─ analysis_options.yaml
dependencies:
  custom_lint: 
    # 绝对路径  
    path: xxx/xxx/custom_lint
  candies_analyzer_plugin: any
  path: any
  analyzer: any
  analyzer_plugin: any

重启服务器

更新代码后,你需要通过以下步骤重启分析服务器:

  1. 找到Command PaletteView

    command_palette

  2. 输入Restart Analysis Server

    analysis_command

现在你可以看到新的改动了。


日志

  1. 性能方面,默认为false,如果你想检查日志,设置为true。你可以在以下位置打开日志:

    CandiesAnalyzerPluginLogger().shouldLog = true;
    

    在项目中会生成custom_lint.log文件。

  2. 你可以自定义日志名称

    CandiesAnalyzerPluginLogger().logFileName = 'your name';
    
  3. 记录信息

    CandiesAnalyzerPluginLogger().log(
        'info',
        // 日志文件生成的位置
        root: result.root,
      );
    
  4. 记录错误

    CandiesAnalyzerPluginLogger().logError(
      'analyze file failed:',
      root: analysisContext.root,
      error: e,
      stackTrace: stackTrace,
    );
    

配置

禁用一个分析项

默认情况下,所有自定义分析项都是启用的。你也可以在analysis_options.yaml中编写配置来禁用它们。

  1. 为分析项添加忽略

    analyzer:
      errors:
        perfer_candies_class_prefix: ignore
    
  2. 排除文件

    analyzer:
      exclude:
        - lib/exclude/*.dart
    
  3. 禁用一个分析项

    linter:
      rules:
        # 禁用一个分析项
        perfer_candies_class_prefix: false 
    
包含

我们可以在custom_lint(即你的插件名)下定义include标签。这意味着我们只分析包含的文件。

# 你的插件名
custom_lint:
  # 如果我们定义了这个,我们就只分析包含的文件
  include: 
    - lib/include/*.dart
自定义分析项的严重性

你可以通过以下设置改变分析项的严重性。

perfer_candies_class_prefix的严重性从info改为warning

支持warninginfoerror

analyzer:
  errors:
    # 覆盖错误严重性
    perfer_candies_class_prefix: warning

默认分析项

PerferClassPrefix

定义一个类名以前缀开头

class PerferClassPrefix extends DartLint {
  PerferClassPrefix(this.prefix);

  final String prefix;

  [@override](/user/override)
  String get code => 'perfer_${prefix}_class_prefix';
}
PreferAssetConst

偏好使用资产常量而不是字符串。

class PreferAssetConst extends DartLint {
  [@override](/user/override)
  String get code => 'prefer_asset_const';
  [@override](/user/override)
  String? get url => 'https://pub.dev/packages/assets_generator';  
}
PreferNamedRoutes

偏好使用命名路由。

class PreferNamedRoutes extends DartLint {
  [@override](/user/override)
  String get code => 'prefer_named_routes';
  [@override](/user/override)
  String? get url => 'https://pub.dev/packages/ff_annotation_route';  
}
PerferSafeSetState

偏好在调用setState之前检查mounted状态。

class PerferSafeSetState extends DartLint {
  [@override](/user/override)
  String get code => 'prefer_safe_setState';
}
MustCallSuperDispose

实现此方法的实现应以调用继承的方法结束,如super.dispose()

class MustCallSuperDispose extends DartLint with CallSuperDisposeMixin {
  [@override](/user/override)
  String get code => 'must_call_super_dispose';
}
EndCallSuperDispose

应该在该方法末尾调用super.dispose()

class EndCallSuperDispose extends DartLint with CallSuperDisposeMixin {
  [@override](/user/override)
  String get code => 'end_call_super_dispose';
}
PerferDocComments

偏好使用文档注释。

class PerferDocComments extends DartLint {
  [@override](/user/override)
  String get code => 'perfer_doc_comments';
}
PreferSingleton

这不是单例,每次都会创建新对象。

class PreferSingleton extends DartLint {
  [@override](/user/override)
  String get code => 'prefer_singleton';
}
GoodDocComments

错误的注释格式。(/// xxx)用于公共API,(// xxx)用于其他情况。

class GoodDocComments extends DartLint {
  [@override](/user/override)
  String get code => 'good_doc_comments';
}
PreferTrailingComma

偏好使用尾随逗号以获得更好的代码风格。

class PreferTrailingComma extends DartLint {
  [@override](/user/override)
  String get code => 'prefer_trailing_comma';
}

完成

创建自定义完成项

你可以在completionContributors中定义自己的CompletionContributor,默认的是ExtensionMemberContributor

/// The completionContributors to finish CompletionRequest
List<CompletionContributor> get completionContributors => [
      ExtensionMemberContributor(),
    ];
扩展成员的建议

尽管Dart团队已经关闭了问题[Auto import (or quickfix?) for Extensions · Issue #38894 · dart-lang/sdk (github.com)],但在不同IDE开发时仍然存在许多问题。

ExtensionMemberContributor有助于轻松处理扩展成员。


预提交

pre_commit.dart

找到基于以下项目结构的pre_commit.dart文件,这是一个在提交代码前检查错误的示例。

├─ example
│  ├─ custom_lint
│  │  └─ tools
│  │     └─ analyzer_plugin
│  │        ├─ bin
│  │        │  └─ pre_commit.dart
预提交脚本

运行candies_analyzer_plugin --pre-commit,预提交脚本将在.git/hooks下生成。

你可以在项目中添加一个pre-commit文件来自定义这个脚本模板。

├─ example
│  ├─ pre-commit

{0}{1}是占位符,不要修改它们。

#!/bin/sh

# 项目路径
base_dir="{0}"

dart format "$base_dir"

# pre_commit.dart路径
pre_commit="{1}"
 
echo "正在检查代码提交前的状态..."
echo "正在分析$base_dir..."

info=$(dart "$pre_commit" "$base_dir")

echo "$info"

if [[ -n $info && $info != *"No issues found"* ]];then
exit 1
fi

当你运行git commit命令提交代码时,它将首先在.git/hooks/pre-commit脚本中运行。 然后该脚本将调用example/custom_lint/tools/analyzer_plugin/bin/pre_commit.dart,如果有错误,你应该退出1。

你可以修改example/pre-commitexample/custom_lint/tools/analyzer_plugin/bin/pre_commit.dart来自定义你的规则。

缓存错误到文件

设置CandiesAnalyzerPlugin.cacheErrorsIntoFile为true,以减少在提交代码前检查错误所花费的时间。


注意事项

打印延迟

在插件分析过程中不要写print语句,这会导致分析延迟。

pubspec.yaml和analysis_options.yaml

你必须做以下事情来支持你的项目进行分析:

  1. pubspec.yamldev_dependencies中添加custom_lint,参见pubspec.yaml

  2. analysis_options.yamlanalyzer plugins中添加custom_lint,参见analysis_options.yaml

仅在vscode中支持Dart文件的快速修复(Android Studio支持任何类型的文件)

issue

在vscode中自动导入不工作

issue


示例代码

// ignore_for_file: unused_local_variable

import 'package:example/singleton.dart';
import 'package:flutter/material.dart';
import 'package:flutter_package/flutter_package.dart' as flutter_package;
import 'test.dart' as test;

void main() {
  int i = 1;
  i.f(1);
  flutter_package.TestApi.common.aaa();
  Singleton().printInfo();
  var num = Singleton().num;
  const Singleton1().printInfo();
  var num1 = const Singleton1().num;
  test.Test test1 = test.Test();
  List<int> list = [];

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  [@override](/user/override)
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void test() {
    setState(() {
      _counter++;
    });
  }

  [@override](/user/override)
  void dispose() {
    super.dispose();
    int i = 1;
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            Image.asset('name'),
            GestureDetector(
              onTap: () => setState(() {}),
            ),
            GestureDetector(
              onTap: () {
                Navigator.push(
                  context,
                  MaterialPageRoute(builder: (b) => Container()),
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

更多关于Flutter数据分析插件candies_analyzer_plugin的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter数据分析插件candies_analyzer_plugin的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter项目中使用candies_analyzer_plugin插件进行数据分析的示例代码案例。请注意,这个示例假设你已经有一个Flutter项目,并且已经在pubspec.yaml文件中添加了candies_analyzer_plugin依赖。

首先,确保你的pubspec.yaml文件中包含以下依赖项:

dependencies:
  flutter:
    sdk: flutter
  candies_analyzer_plugin: ^最新版本号  # 请替换为实际的最新版本号

然后,运行flutter pub get来安装依赖。

接下来,在你的Flutter项目中,你可以按照以下步骤使用candies_analyzer_plugin进行数据分析。

1. 导入插件

在你的Dart文件中导入插件:

import 'package:candies_analyzer_plugin/candies_analyzer_plugin.dart';

2. 初始化插件

通常,你会在应用的入口文件(如main.dart)中初始化插件:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  // 初始化插件
  CandiesAnalyzerPlugin.instance.initialize();
  runApp(MyApp());
}

3. 使用插件进行数据分析

假设你想要分析一些用户行为数据,你可以创建一个方法来收集和处理这些数据。以下是一个简单的示例,展示如何使用插件提供的功能:

class DataAnalyzer {
  CandiesAnalyzerPlugin _analyzerPlugin = CandiesAnalyzerPlugin.instance;

  Future<void> analyzeUserBehavior(Map<String, dynamic> userData) async {
    try {
      // 假设插件提供了一个分析用户行为的方法
      await _analyzerPlugin.analyze(userData);
      print("User behavior data analyzed successfully.");
    } catch (e) {
      print("Error analyzing user behavior data: $e");
    }
  }
}

4. 在UI中触发数据分析

在你的UI组件中,你可以触发数据分析。例如,在一个按钮点击事件中:

class MyApp extends StatelessWidget {
  final DataAnalyzer _dataAnalyzer = DataAnalyzer();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Candies Analyzer Plugin Demo'),
        ),
        body: Center(
          child: ElevatedButton(
            onPressed: () async {
              // 创建一些示例用户数据
              Map<String, dynamic> userData = {
                'userId': '12345',
                'action': 'page_view',
                'timestamp': DateTime.now().toIso8601String(),
                'page': '/home',
              };
              
              // 分析用户行为数据
              await _dataAnalyzer.analyzeUserBehavior(userData);
            },
            child: Text('Analyze User Behavior'),
          ),
        ),
      ),
    );
  }
}

注意事项

  • 上述代码中的CandiesAnalyzerPlugin.instance.analyze(userData);是一个假设的方法调用。实际使用时,你需要参考candies_analyzer_plugin的文档来确定正确的方法调用和参数。
  • 插件的具体功能和使用方式可能会有所不同,因此务必查阅最新的官方文档或插件的README文件。
  • 确保你已经正确配置了任何必要的权限(如网络访问权限),这取决于插件的具体需求。

这个示例提供了一个基本的框架,展示了如何在Flutter项目中集成和使用candies_analyzer_plugin进行数据分析。根据插件的实际功能和需求,你可能需要调整代码以适应你的具体用例。

回到顶部