Background

Dart Conditional Import official documentation:https://dart.dev/guides/libraries/create-library-packages#conditionally-importing-and-exporting-library-files

Dart Conditional Import itself is a syntax element that exists in Dart 1 and 2. It used to support command line control on the Dart side. However, in the Compiler FrontEnd after Dart 2, this function is converged to only support the check scheme of dart.library, which is used for judging the Flutter web environment:

export 'src/none.dart' // Stub implementation
    if (dart.library.io) 'src/io.dart' // dart:io implementation
    if (dart.library.html) 'src/html.dart'; // dart:html implementation

As shown above, when io is in stock, importsrc/io.dartFiles, and import when in a web environmenthtmlThe corresponding warehouse.

Conditional Import can be effectively used as a Dart Compiler engine precompilation reference function, which can determine the reference logic and order of some code at the compilation time, which is very useful. Therefore, after _1-20, we have improved the user-defined function of Conditional Import through customized modifications for Compiler.

Introduction to Custom Conditional Import

The following Conditional Import uses the Hook Coverage use case.

Here is a use case of Conditional import under Hook Plugin, see [Byte Dart Hook Stake Coverage SDK Access Documentation]()

import 'package:hook_coverage/empty.dart'
    if (condition.hooked) 'package:hook_coverage/hook_coverage.dart';

The precompilation semantics of the above code are:

  • Default import in code package:hook_coverage/empty.dartSuch a near-empty code file.
  • And when the user-defined conditions, namelycondition.hookedWhen the value is true,Turn to import package:hook_coverage/hook_coverage.dart';Such a file.

In this way, users can either import empty when not passing conditions without affecting normal program development and packaging, and the code related to Coverage is not processed at the compilation time in the TFA of AOT, so it will not be entered into the final result product. It will not affect the package size and running efficiency when the Coverage function is not turned on.

Custom Conditional Import Usage

Abovecondition.hookedIs a condition that can be supported on the command line and configuration files. If there is no relevant condition injection, it means the default is false:

  1. #### Declare macro conditions in pubspec.yaml
conditions:
 condition.hooked: true

Remember that all conditions must be based oncondition.As a declared constraint at the beginning, the value can be set to true or false. Conditions that will not change for a long time are recommended to be placed here for processing.

  1. #### Declare macro conditions on the command line
flutter run --conditions condition.hooked=true,conditions.xxxx=false

As shown above, we can also declare conditions on any command line of flutter, and multiple conditions can be declared at the same time (use,As a dividing line).The conditions of the command line can coincide with the conditions of the configuration file, and the conditions of the command line will override the conditions in the configuration file.

Use scenario

Hook Plugin scene adaptation scheme

See the above and,[Byte Dart Hook Stake Coverage SDK Access Documentation]() 。

The access scenario of the Hook Plugin is shown in the following code:

import 'package:hook_coverage/empty.dart'
    if (condition.hooked) 'package:hook_coverage/hook_coverage.dart';


void main() {
  uploader.start(Duration(seconds: 30));
  runApp(MyApp());
} 

In addition to the introduction of Conditional Import explained here, we also encountered the needuploader.start(Duration(seconds: 30));To register the statement of uploading Coverage regularly, here because there may be two import relationships:

  1. Empty files are introduced by default
  2. Hook open introductionhook_coverageFile introduction

Because the main function we include inhook_coverageAmong, thereforeemptyWe also saved an implementation for compatibility:

library empty;


// We import nothing !


class Any {
  @override
  dynamic noSuchMethod(Invocation invocation) {
    // ignore no such method.
    return Any();
  }
}


final dynamic uploader = Any();

We use dynamic objects to call mechanisms that can call methods that do not exist at compile time, and overload the object’snoSuchMethodThe method allows us toemptyOn the premise of being introduced, it can also be introduced into the corresponding uploader object and call the corresponding method.

Adaptation of UME usage scenarios

UME is a debugger tool during Flutter operation. It provides rich functions such as network grab, exception collection, ruler, performance billboard, etc. When accessing, due to the need to display floating windows, FloatingWidgets need to be nested on the outermost Widget:

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]).then((_) {
    runApp(
      FloatingWidget(child: App(), enable: true),
    );
  });    
}

FloatingWidgets are nested on top of the actual App Widget as above.

However, UME is used by debug tools during debugging and should not be taken online. Therefore, we need to implement a method that can support deleting the corresponding import in the TFA process and prevent UME-related code from being included in the final packaging process. Therefore, we need to provide an empty FloatingWidget implementation to shield UME-related references:



class FloatingWidget extends StatelessWidget {
FloatingWidget ({Key key, @required this.enable , @ required this.child})
: assert (child != null),
Assert (enable != null),
Super (key: key);
To enable final bool final bool;
Widget final final child;


// returns nested Child data directly
@Override
Widget build (BuildContext context) {
To return the child;
}
}

For example, as above, we simply wrote a nested widget to handle it and directly return the nested Child object. Even we can not introduce an additional class, for example, we modify the access method of the UME to a static method:

Widget addUMEWidget(Widget child, bool enable) {
    return FloatingWidget(child, enable: enable);
}

It also provides an empty method implementation, assuming that it is stored in'package:xg_ume/empty.dartAmong:

Widget addUMEWidget(Widget child, bool enable) {
    return child;
}

Therefore, the scheme we introduced can be modified to:

import 'package:xg_ume/empty.dart'
    if (condition.open_ume) 'package:xg_ume/xg_ume.dart';


void main() {
   return runApp(addUMEWidget(App(), true));
}

When you need to compile UME (debug mode):

flutter run --conditions condition.open_ume=true

When you need a release package:

Flutter build apk // default pass parameter is false, you can also manually specify false

Won’t introduce'package:xg_ume/xg_ume.dart'The header file and related code will not enter the final product.

Multi-version engine usage scenarios

From the above discussion, we can see that Conditional Import itself can be used as a syntax-supported ** precompilation macro in the Dart layer, so we can flexibly provide in Flutter Framework** Conditions of the current version to provide adaptation schemes for different scenarios and corresponding version control mechanisms.

For example, in the 1.12.13 engine, we have these conditions

        final conditionsFile = FlutterProject.current().directory.childFile('build/conditions');
        String conditions;
        final List<String> allConditions = [
          'condition.release=${buildMode == BuildMode.release}',
          'condition.debug=${buildMode == BuildMode.debug}',
          'condition.notAot=${!aot}',
          'condition.1.12.13=${true}'
        ];


        if (conditionsFile.existsSync()) {
          conditions = conditionsFile.readAsStringSync();
          allConditions.add(conditions);
        }
        conditions = allConditions.join(',');

_1= true is also a specific scenario of the 1.12.13 version of the engine. When our engine is upgraded to 1.20, there will be:

condition.1.12.13 = false
condition.1.20 = true

The above conditions are provided by default in the engine, so we can provide version-specific behaviors in the engine. For example, we can use the same as above for ** UME culling scheme and Hook Plugin culling scheme **1.12.13Filter and develop specific versions as adaptation conditions for projects.

Realization principle

Understand the current scenarios that Conditional Import can use and has used, including but not limited to:

  1. Opening and closing of key modules
  2. Hook function switch
  3. Distinguish between different channel products and multi-flavor channel products for debug release channel products for debug release
  4. Control the adaptation of basic libraries and multi-version engines of business codes.

In the following section, let’s talk about how we implemented the Conditional Import scheme, including how the original functions are, and how we modified the Dart compiler in Flutter to support our above scheme.

Original logic

According to the function of Conditional Import itself, it is in a compilation selection function scenario, that is, above Dart 2.x, it is only used to determine whether the current environment is the Dart default environment or the Web (because the libraries used will be different).

Therefore, according to the familiar Dart Compiler itself, it can be determined that this part of the functional logic should exist in front_end part of the Dart Compiler, belonging to the front end of the compiler.

// pkg/front_end/lib/src/fasta/source/outline_builder.dart
@override
void endConditionalUri(Token ifKeyword, Token leftParen, Token equalSign) {
  debugEvent("EndConditionalUri");
  int charOffset = popCharOffset();
  String uri = pop();
  if (equalSign != null) popCharOffset();
  String condition = popIfNotNull(equalSign) ?? "true";
  Object dottedName = pop();
  if (dottedName is ParserRecovery) {
    push(dottedName);
  } else {
    push(new Configuration(charOffset, dottedName, condition, uri));
  }
}

You can see that in this callback of fasta (the front end of the Dart compiler), you are actually analyzing the syntax of import if to build the AST:

import 'one.dart' if (condition) 'other.dart';

This syntax function, and if-condition is successfully collected, a Configuration configuration item will be generated, including the location, name, condition, and corresponding Uri. When the import element is finally processed, there will be such logic:

// pkg/front_end/lib/src/fasta/source/outline_builder.dart
@override
void endImport(Token importKeyword, Token semicolon) {
  debugEvent("EndImport");
  List<Combinator> combinators = pop();
  bool isDeferred = pop();
  int prefixOffset = pop();
  Object prefix = pop(NullValue.Prefix);
  List<Configuration> configurations = pop();
  int uriOffset = popCharOffset();
  String uri = pop(); // For a conditional import, this is the default URI.
  List<MetadataBuilder> metadata = pop();
  checkEmpty(importKeyword.charOffset);
  if (prefix is ParserRecovery) return;
  library.addImport(
      metadata,
      uri,
      configurations,
      prefix,
      combinators,
      isDeferred,
      importKeyword.charOffset,
      prefixOffset,
      uriOffset,
      importIndex++);
}

And actuallyaddImportWe will find out how this Conditional Import is implemented in the logic of:

void addImport(
    List<MetadataBuilder> metadata,
    String uri,
    List<Configuration> configurations,
    String prefix,
    List<Combinator> combinators,
    bool deferred,
    int charOffset,
    int prefixCharOffset,
    int uriOffset,
    int importIndex) {
  if (configurations != null) {
    for (Configuration config in configurations) {
      if (lookupImportCondition(config.dottedName) == config.condition) {
        uri = config.importUri;
        break;
      }
    }
  }


  imports.add(new Import(this, builder, deferred, prefix, combinators,
      configurations, charOffset, prefixCharOffset, importIndex,
      nativeImportPath: nativePath));
}

We will find that the actual import logic, including the native method of dart-ext, is also uniformly implemented in this method, while our positive main Conditional Import is limited to execution as a compilation condition, because its execution logic will affect the compiler Load logic of the program library.

Next we look at the actuallookupImportConditionThe logic is as follows, very rough:

    String lookupImportCondition(String dottedName) {
      const String prefix = "dart.library.";
      if (!dottedName.startsWith(prefix)) return "";
      dottedName = dottedName.substring(prefix.length);
      if (!loader.target.uriTranslator.isLibrarySupported(dottedName)) return "";


      LibraryBuilder imported =
          loader.builders[new Uri(scheme: "dart", path: dottedName)];


      if (imported == null) {
        LibraryBuilder coreLibrary = loader.read(
            resolve(
                this.uri, new Uri(scheme: "dart", path: "core").toString(), -1),
            -1);
        imported = coreLibrary
            .loader.builders[new Uri(scheme: 'dart', path: dottedName)];
      }
      return imported != null ? "true" : "";
    }

First of all, because only to check the function load, all native conditions need to be useddart.libraryThe prefixes are described, and there is a list of those libraries in the detection that support this way of introduction.

import '_binding_io.dart' if (dart.library.html) '_binding_web.dart' as binding;

Use immediatelyLibraryBuilderTo determine whether the current Dart library (such as html library) has been introduced, use this method to determine whether the current environment is Web, because the web has html libraries that ordinary Dart programs do not have… (Magical thinking).

So the result is to use it if not_binding_webFrom now on, according to the priority of import if, we can also write the case of n + condition superposition.

Developing Customizable Conditional Import Features

Custom conditional judgment

From the principle of secondary development for Dart Compiler, we’d better not modify the syntax logic of Dart itself, otherwise it will cause incompatibility of business-side code logic. Therefore, for us, Conditional Import itself supports the collection of this syntax, and we can completely reuse this syntax logic, so we can skip the collection function of Configuration and directly develop the if-condition judgment function:

    final Map<String, String> importedConditions = <String, String>{};


    String lookupUdfImportCondition(String dottedName) {
      const String prefix = "condition.";
      if (!dottedName.startsWith(prefix)) return "";
      final result = importedConditions[dottedName];
      if (result == null) {
        return "";
      }


      return result;
    }

Referring to the official implementation plan, we also implemented a User-Define lookup logic. We assume that some user-defined condition logic can be collected by passing in externally and stored in aimportedConditionsIn the Map, when lookup, we look for whether these logic is true or false and return “true” or “false.”

We then pass this part of the logic into the’addImport ‘to make calls, so that our conditional judgment will be searched and executed after the system’s Panu single logic. If the running logic is true, the specific import Uri of the import statement will be replaced. The actual precompilation macro function is also imminent:

void addImport(
    List<MetadataBuilder> metadata,
    String uri,
    List<Configuration> configurations,
    String prefix,
    List<Combinator> combinators,
    bool deferred,
    int charOffset,
    int prefixCharOffset,
    int uriOffset,
    int importIndex) {
  if (configurations != null) {
    for (Configuration config in configurations) {
      if (lookupImportCondition(config.dottedName) == config.condition) {
        uri = config.importUri;
        break;
      }
      if (lookupUdfImportCondition(config.dottedName) ==
          config.condition) {
        uri = config.importUri;
        break;
      }
    }
/// extra code.
  }

How to Incoming Conditions?

In the case that the logic of the Compiler processing imports can run smoothly, we have to solve a problem. What if we pass these Conditions from the outside world? Here we give two cases of passing custom Conditions as described in the above introduction:

  • Usepubspec.yamlFor persistent parameter configuration.
  • Use command line--conditionsPassing parameters on the command line is easier to configure on the CI.

The implementation of these two schemes is not troublesome, we can explain them together.

  1. pubspec.yamlBiography:

Before that, we can add it to Dart’s command linepubspecThe specific location of the file is used for persistence and reading configuration parameters, so we just need to read it again:

    // yaml logic
    final pubspec = pOptions.pubspecFile;
    if (pubspec != null) {
      final pubspecNode = yaml.loadYaml(
        File.fromUri(Uri.file(pubspec)).readAsStringSync(),
      );
      final yaml.YamlMap conditions = pubspecNode['conditions'];
      if (conditions != null && conditions is yaml.YamlMap) {
        for (String key in conditions.keys) {
          importedConditions[key] = conditions[key].toString();
        }
      }
    }

We read the corresponding condition from the yaml file and put it into the importedConditions of the Compiler.

  1. Command line parameters:

In the command line parameters, we directly added the multi-params parameter of conditions to the command line of the Dart program, so we read the corresponding parameters and read the conditions into the Compiler under decomposition 👌.

    // params logic
    if (pOptions.conditions.isNotEmpty) {
      pOptions.conditions.forEach((entity) {
        final con = entity.split('=');
        assert (con != null && con.length == 2);
        importedConditions[con[0]] = con[1];
      });
    }

Summary

Conditional Import gives a technical scheme of “precompilation macro” that is non-intrusive to syntax under Dart program. It has certain practice and use in dealing with compilation parameters to control the introduction of program compilation and the scene of multi-version engine adaptation. This article introduces how to use Conditional Import, the actual landing and the actual implementation principle analysis in Compiler. We can see that the implementation of Condition Import in the compiler isVery simple and elegant, this may give us a reminder of our daily development“The modification function itself is not the point, the point is to know where to modify”Therefore, it is still necessary to read and observe more other Compiler or large-scale programming schemes in daily life to exercise the ability to quickly sort out the structure and function of projects in brain topology.