background

When Flutter development is used, there are sometimes problems with many image resources. According to regulations, image resources must be configured in pubspec.yaml file to be used properly. If there are more than 50 image resources, do you need to configure them one by one? Obviously not!

Yaml allows Flutter to configure the image resource folder path directly in pubspec.yaml. There is no need to specify each image resource path.

Different developments take a different approach, but here I use the Dart annotation, which is a one-line annotation to configure the resource file.

For example, we have the following resource files:

We only need to configure one line of comment:

@ImagePathSet('assets/'.'ImagePathTest')
void main() => runApp(MyApp());
Copy the code

Then run a command:

flutter packages pub run build_runner build
Copy the code

Dart generates a.dart file with the following contents:

class ImagePathTest {
  ImagePathTest._();

  static const BANNER = 'assets/image/banner.png';
  static const PLAY_STOP = 'assets/image/play_stop.png';
  static const SAVE_BUTTON = 'assets/image/save_button.png';
  static const MINE_HEADER_IMAGE = 'assets/image/test/mine_header_image.png';
  static const VERIFY = 'assets/image/verify.png';
  static const VERIFY_ERROR = 'assets/image/verify_error.png';
}
Copy the code

At the same time, automatically configure our resource files in the pubspec.yaml folder:

assets:
- assets/image/banner.png
- assets/image/play_stop.png
- assets/image/save_button.png
- assets/image/test/mine_header_image.png
- assets/image/verify.png
- assets/image/verify_error.png
Copy the code

When used, you can directly reference image resources in code form:

Image.asset(ImagePathTest.xxx)
Copy the code

That is to prevent hand error on business, and can improve efficiency ~ the following focus on our development ideas.


Generate a resource configuration file using a one-line annotation

For details on Dart annotation use, see this article on Flutter annotation handling and code generation, as well as the official note on source_gen

As a quick note, the source_gen documentation reads:

source_gen is based on the build package and exposes options for using your Generator in a Builder. Part of the document is omitted…… In order to get the Builder used with build_runner it must be configured in a build.yaml file.

Translated into Chinese:

Source_gen is based on the Build package and provides exposed options for using your own Generator in a Builder. . To be able to use Builder and build_runner together, a build.yaml file must be configured.

So to use the Dart annotation, we need to do a few things:

  • Dependency annotation librarysource_gen
  • Dependency build runtimebuild_runner
  • Create an annotation code generatorGenerator
  • createBuild
  • createbuild.yamlfile
  • Introduce relevant annotations where they are needed
  • Run the build command to build

Let’s go one at a time.

Dependency annotation librarysource_gen

There’s nothing to say about this, but you must rely on this library for Dart annotations:

dependencies:
  source_gen: ^ 0.9.4 + 5
Copy the code

For the version, see source_gen here

Dependency build runtimebuild_runner

Dev_dependencies = dev_dependencies

dev_dependencies:
  build_runner: ^ 1.7.1
Copy the code

The build_Runner version is available here

Pub run build_runner is built into the library, which includes the following four types of compilation:

  • build
  • watch
  • serve
  • test

The first construction method is generally required in FLUTTER. The four commands above can be supplemented with commands such as –delete-conflicting-outputs. For details, see build_runner here

Create an annotation code generatorGenerator

Literally translated as generator, the official description is:

A tool to generate Dart code based on a Dart library source.

A tool for generating Dart code based on the source code of the Dart library. The class diagram is as follows:

Both classes are abstract classes, usually create a class inherited from GeneratorForAnnoration and implement the abstract method, in the abstract method we need to complete the development of logical functions; Here we need to create a picture resource file configuration function, which has the following two main points:

  • Requires the consumer to specify the resource folder path
  • The consumer is required to specify the name of the resource configuration class to be generated

So let’s create an instance class that contains these two information:

class ImagePathSet{
  /// Resource folder path
  final String pathName;
  
  /// Name of the resource configuration class to be generated
  final String newClassName;

   const ImagePathSet(this.pathName, this.newClassName);
}
Copy the code

Eventually the first reference we use will look like this:

@ImagePathSet('assets/'.'ImagePathTest')
Copy the code

The important thing to note here is that the constructor of this class must be const. With the final annotation class we need to use, we create the generator:

class ImagePathGenerator extends GeneratorForAnnotation<ImagePathSet> {
  @override
  generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
    
    return null; }}Copy the code

In generateForAnnotatedElement () method, we can complete the logical part of our development, it involves three parameters:

  • Element: This is the detail of the annotated class/method, such as the modified part of the code:

    /// path_test.dart
    
    @ImagePathSet('assets/'.'ImagePathTest')
    class PathTest{}
    Copy the code

    Some element information is as follows:

    element.name                /// PathTest
    element.displayName         /// PathTest
    element.toString()          /// class PathTest
    element.enclosingElement    /// /example/lib/path_test.dart
    element.kind                /// CLASS
    element.metadata            /// [@ImagePathSet ImagePathSet(String pathName, String newClassName)]
    Copy the code
  • The two most commonly used methods are:

    • read(String field)
    • peek(String field)

    Both read the given annotation parameter information, and the former returns NULL if a FormatException is not read or thrown. Note that these two methods return a ConstantReader type. If you want to get the value of a specific annotation element, you need to call the corresponding xxxValue method. XXX indicates the specific type.

    String pathName= annotation.peek('pathName').stringValue
    Copy the code

    If we do not know the type of the annotation parameter, we can use isXxx to determine whether it is the corresponding type, for example:

    annotation.peek('pathName').isString    ///true
    annotation.peek('pathName').isInt       ///false
    Copy the code
  • Dart does not support reflection. We did not find a way to do this.

    • InputId: contains information entered during build

The complete generator code for generating resource files is as follows:

import 'dart:io';

import 'package:analyzer/dart/element/element.dart';

import 'package:image_path_helper/image_path_set.dart';
import 'package:source_gen/source_gen.dart';
import 'package:build/build.dart';


class ImagePathGenerator extends GeneratorForAnnotation<ImagePathSet> {
  String _codeContent = ' ';
  String _pubspecContent = ' ';

  @override
  generateForAnnotatedElement(Element element, ConstantReader annotation, BuildStep buildStep) {
    final explanation = '// **************************************************************************\n'
        '// If there are new files that need to be updated, it is recommended to run the clean command: \n'
        '// flutter packages pub run build_runner clean \n'
        '// \n'
        '// Then run the following command to regenerate the corresponding file: \n'
        '// flutter packages pub run build_runner build --delete-conflicting-outputs \n'
        '/ / * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *';

    var pubspecFile = File('pubspec.yaml');

    for (String imageName in pubspecFile.readAsLinesSync()) {
      if (imageName.trim() == 'assets:') continue;
      if (imageName.trim().toUpperCase().endsWith('.PNG')) continue;
      if (imageName.trim().toUpperCase().endsWith('.JPEG')) continue;
      if (imageName.trim().toUpperCase().endsWith('.SVG')) continue;
      if (imageName.trim().toUpperCase().endsWith('.JPG')) continue;
      _pubspecContent = "$_pubspecContent\n$imageName";
    }
    _pubspecContent = '${_pubspecContent.trim()}\n\n assets:'; Var imagePath = annotation. Peek ('pathName').stringValue;
    if(! imagePath.endsWith('/')) {
      imagePath = '$imagePath/'; Var newClassName = annotation.peek('newClassName').stringValue; HandleFile (imagePath); / / / add images path to pubspec. The yaml file pubspecFile. WriteAsString (_pubspecContent); /// return the generated code filereturn '$explanation\n\n'
        'class $newClassName{\n'
        ' $newClassName._(); \n'
        ' $_codeContent\n'
        '} ';
  }

  void handleFile(String path) {
    var directory = Directory(path);
    if (directory == null) {
      throw '$path is not a directory.';
    }

    for (var file in directory.listSync()) {
      var type = file.statSync().type;
      if (type == FileSystemEntityType.directory) {
        handleFile('${file.path}/');
      } else if (type == FileSystemEntityType.file) {
        var filePath = file.path;
        var keyName = filePath.trim().toUpperCase();

        if(! keyName.endsWith('.PNG') &&
            !keyName.endsWith('.JPEG') &&
            !keyName.endsWith('.SVG') &&
            !keyName.endsWith('.JPG')) continue;
        var key = keyName
            .replaceAll(RegExp(path.toUpperCase()), ' ')
            .replaceAll(RegExp('.PNG'), ' ')
            .replaceAll(RegExp('.JPEG'), ' ')
            .replaceAll(RegExp('.SVG'), ' ')
            .replaceAll(RegExp('.JPG'), ' ');

        _codeContent = '$_codeContent\n\t\t\t\tstatic const $key = \'$filePath\'; '; // use \t symbol instead of space. _pubspecContent ='$_pubspecContent\n - $filePath'; }}}}Copy the code

Create a Build

The main purpose of a Build is to get the generator to execute. Here we create a Build like this:

Builder imagePathBuilder(BuilderOptions options) =>
    LibraryBuilder(ImagePathGenerator());
Copy the code

The main referenced packages are:

import 'package:build/build.dart';
Copy the code

Create the build.yaml file

Here our build.yaml file is configured as follows:

builders:
  image_builder:
    import: 'package:image_path_helper/builder.dart'
    builder_factories: ['imagePathBuilder']
    build_extensions: { '.dart': ['.g.dart'] }
    auto_apply: root_package
    build_to: source
Copy the code

The build.yaml configuration information is eventually read by the BuildConfig class in build_config.dart. For parameter descriptions, the official description build_config is recommended here.

A complete build.yaml structure is shown below:

build.yaml
BuildConfig
build.yaml
BuildConfig
BuildConfig

key value default
targets Map<String, BuildTarget> A single target should have the same package name
builders Map<String, BuilderDefinition> /
post_process_builders Map<String, PostProcessBuilderDefinition> /
global_options Map<String, GlobalBuilderOptions> /

The four key pieces of information correspond to the four root nodes in the build.yaml file, of which the builders node is the most commonly used.

Builders that

Builders configure the configuration information for all the Builders in your package in the format Map

. For example, we have a Builder that looks like this:
,>

/// builder.dart
Builder imagePathBuilder(BuilderOptions options) =>
    LibraryBuilder(ImagePathGenerator());
Copy the code

We can configure this in the build.yaml file:

builders:
  image_builder:
    import: 'package:image_path_helper/builder.dart'
    builder_factories: ['imagePathBuilder']
    build_extensions: { '.dart': ['.g.dart'] }
    auto_apply: root_package
    build_to: source
Copy the code

Image_builder corresponds to the String part of Map

, and the BuilderDefinition information following: corresponds to the structure diagram above. Let’s break down the information for each parameter in BuilderDefinition:
,>

parameter The parameter types instructions
builder_factories List<String> Mandatory parameter, returnedBuilderA list of method names, such as the one aboveBuilderMethod is calledimagePathBuilderWritten,['imagePathBuilder']
import String Mandatory parameter, importBuilderPackage path, in the format ofPackage: uriThe string
build_extensions Map<String, List<String>> Mandatory parameter, mapping from input extension to output extension. For example: for example, the format of the file where the annotation is used is.dartTo specify that the output file format can be specified by.dartConverted to.g.dart.zipAnd so forth
auto_apply String This parameter is optional. The default value isnone, corresponding to the source codeAutoApplyEnumerated class, with four optional configurations:

  • none: Do not apply this generator unless you configure it manually
  • dependents: Applying this Builder to a package depends directly on the package that exposes the Builder
  • all_packages: Applies this builder to all packages in the pass dependency diagram
  • root_package: Apply this generator only to top-level packages

  • Do you feel confused? It doesn’t matter, explained separately below ~~~

    required_inputs List Optional argument, used to adjust the build order, specifying one or a series of file extensions to run after any Builder that might produce this type of output
    runs_before List<BuilderKey> An optional parameter to adjust build sequentially, which is the opposite of the one above, to run before the specified Builder

  • BuilderKey: indicates atargetThe identity symbol, mainly by the correspondingBuilderPackage name and method name, for exampleimage_path_helper|imagePathBuilder
  • applies_builders List<BuilderKey> Optional parameter,BuilderThe list of keys, which is the identity tag, followsbuilder_factoriesThe parameter configuration should be one-to-one
    is_optional bool This parameter is optional. Default valuefalseTo specify whether running can be delayedBuilderIn general, no configuration is required
    build_to String This parameter is optional. The default value iscache, mainly for theBuildToTwo arguments to an enumeration class:

  • cache: Output goes into the hidden build cache and will not be published
  • source: The output goes into the source tree next to its main input

    If you need to compile files that can be seen in your own source code, set this parameter tosourceIf the specified generator returnsnullIf no file generation is required, set this parameter tocache
  • defaults TargetBuilderConfigDefaults Optional parameter: The user is not in itbuilders[Here refers totargetsUnder the nodebuildersDon’t get confused! 】 Section to apply when specifying the corresponding key

    Details about the auto_apply parameter:



    Annotations package

    • When we willauto_applySet todependentsWhen:
      • ifAnnotations packageIs directly dependent onsub_package02On, then only insub_package02On normal use of annotations, thoughPackagePackage depends on thesub_package02, but the annotation still cannot be used properly
    • When we willauto_applySet toall_packagesWhen:
      • ifAnnotations packageIs directly dependent onsub_package02On, then insub_package02PackageAnnotations can be used normally
    • When we willauto_applySet toroot_packageWhen:
      • ifAnnotations packageIs directly dependent onsub_package02On, then only inPackageOn normal use of annotations, althoughsub_package02Do depend on, but just don’t give use
    • Therefore, ifAnnotations packageIs directly dependent onPackageWhen I do, I don’t careauto_applySet up thedependents,all_packagesOr is itroot_packageWhen, in fact, are normal use!

    As for the other three parameters of build.yaml, to be honest, I have to skip them here because I don’t use them much at present and only understand some of them.

    Introduce relevant annotations where needed & run build commands to build

    Once this is done, we need to reference the annotations, for example on the main() method:

    @ImagePathSet('assets/'.'ImagePathTest')
    void main() => runApp(MyApp());
    Copy the code

    After referencing the annotations, we then execute the following command on the Terminal command line to complete the compilation:

    flutter packages pub run build_runner build
    Copy the code

    Dart file is configured on the main method of the main.dart file. The final generated file is main.g.art. Dart you can refer to the runBuilder method and the Expected_outputs method under run_Builder. dart.

    Note: If you need to rebuild it is recommended to clean it first:

    flutter packages pub run build_runner clean
    Copy the code

    In addition, it is recommended to execute the following command at build time:

    flutter packages pub run build_runner build --delete-conflicting-outputs
    Copy the code

    At this point, a complete annotation with one line of code + one line command to complete the image file configuration function is done ~~~ ~


    Conclusion moment

    To make annotations in Flutter, you only need to follow certain steps and follow your own logic to develop the relevant features easily. The main process steps are summarized as follows:

    • Rely onsource_genbuild_runner
    • Annotation class creation and generator creationGenerator
    • createBuilder
    • Create and configurebuild.yamlfile
    • Reference the created annotations and run the commands to do so

    Tips: This article source location: image_path_helper