After experiencing the project development experience of Flutter, there is no doubt that there will be a lot of confusion. The first question that came to my mind was, how did the mobile host APP integrate the Flutter code we wrote?

This article first looks at how iOS projects integrate Flutter code by platform. My generation analysis is based on this project.

Pod

We open the iOS project with Xcode, and the main project has very little code in it.

The first thing that comes to mind is that we’ve added some dependencies using CocoaPod, so let’s look at the Podfile dependency configuration file.

Podfile
Generated. Xcconfig def flutter_root generated_xcode_build_settingS_PATH = file.expand_path (file.join ('.. ', 'Flutter', 'Generated.xcconfig'), __FILE__) unless File.exist? (generated_xcode_build_settings_path) raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" end File.foreach(generated_xcode_build_settings_path) do |line| matches = line.match(/FLUTTER_ROOT\=(.*)/) return matches[1].strip if matches end raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, Then run flutter pub get" end // 2 introduce podhelper.rb require file. expand_path(file. join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) flutter_ios_podfile_setup target 'Runner' do use_frameworks! use_modular_headers! // 3 Run the flutter_install_all_iOS_Pods method in podhelper.rb File.dirname(file.realPath (__FILE__)) end // 4 Run the flutter_additional_iOS_build_settings method post_install do in Podhelper. rb |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) end endCopy the code

1. Check the environment variables file —Generated.xcconfig

Make sure that the “Flutter” folder in your iOS project contains the Generated. Xcconfig file that defines variables related to Flutter and Dart. Such as FLUTTER_ROOT,FLUTTER_APPLICATION_PATH,FLUTTER_TARGET, etc., which provide the basis for subsequent Pod dependencies.

Generated.xcconfig
FLUTTER_ROOT= /Users/*/Documents/flutter FLUTTER_APPLICATION_PATH=/Users/*/Documents/FlutterVideos/feibo_movie/feibo_movie FLUTTER_TARGET=/Users/chongling.liu/Documents/FlutterVideos/feibo_movie/feibo_movie/lib/main.dart FLUTTER_BUILD_DIR=build SYMROOT=${SOURCE_ROOT}/.. /build/ios OTHER_LDFLAGS=$(inherited) -framework Flutter FLUTTER_FRAMEWORK_DIR = / Users / * / Documents/flutter/bin/cache/artifacts/engine/ios FLUTTER_BUILD_NAME = 1.0.0 FLUTTER_BUILD_NUMBER=1 DART_DEFINES=flutter.inspector.structuredErrors%3Dtrue DART_OBFUSCATION=false TRACK_WIDGET_CREATION=true TREE_SHAKE_ICONS=false PACKAGE_CONFIG=.packagesCopy the code

2. The introduction ofpodhelper.rbfile

Podhelper.rb defines some poD-related methods in the FLUTTER_ROOT/packages/flutter_tools/bin folder.

3. Performpodhelper.rbIn theflutter_install_all_ios_podsmethods

  • flutter_install_all_ios_podsCall theflutter_install_ios_engine_podandflutter_install_ios_plugin_podsMethods, which configure the Flutter engine and third-party libraries, respectively.
def flutter_install_all_ios_pods(ios_application_path = nil)
  flutter_install_ios_engine_pod(ios_application_path)
  flutter_install_ios_plugin_pods(ios_application_path)
end
Copy the code
  • flutter_install_ios_engine_podChinese is mainly willFlutterThe engine isFlutter.frameworkandFlutter.podspecThese two files fromFLUTTER_ROOT/bin/cache/artifacts/engine/iosCopy to iOS projectFlutterFolder, and then configure dependencies
pod 'Flutter', :path => 'Flutter'
Copy the code
Def flutter_install_iOS_engine_pod (iOS_application_path = nil) system('cp', '-r', File.expand_path('Flutter.framework', debug_framework_dir), copied_flutter_dir) system('cp', File.expand_path('Flutter.podspec',debug_framework_dir), copied_flutter_dir) pod 'Flutter', :path => 'Flutter' endCopy the code
  • flutter_install_ios_plugin_podsIs to configure theFlutterLibraries depend on third partiesiOSLibraries or iOS file dependencies.

It’s a little convoluted, for example. Our FLutter code uses the SQflite library. Sqflite calls the FMDB library in iOS, so we need to configure the dependency of FMDB.

def flutter_install_ios_plugin_pods(ios_application_path = nil) plugins_file = File.join(ios_application_path, '.. ', '.flutter-plugins-dependencies') plugin_pods = flutter_parse_plugins_file(plugins_file) plugin_pods.each do |plugin_hash| plugin_name = plugin_hash['name'] plugin_path = plugin_hash['path'] if (plugin_name && plugin_path) symlink = File.join(symlink_plugins_dir, plugin_name) File.symlink(plugin_path, symlink) pod plugin_name, :path => File.join('.symlinks', 'plugins', plugin_name, 'ios') end end endCopy the code

The process of this method is to read the “.flutter-plugins-dependencies “file in the same directory as the iOS file, read the” iOS array “in the” plugins “field, and configure dependencies for each element of the array.

Pod 'sqflite' : path = > 'FLUTTER_ROOT/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/ios'Copy the code
.flutter-plugins-dependencies
{ "plugins":{ ... "Ios" : [{" name ":" sqflite ", "path" : "/ Users / * / Documents/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/", "dependencies":[ ] } ... ] }}Copy the code

4. Performpodhelper.rbIn theflutter_additional_ios_build_settingsmethods

This is ENABLE_BITCODE set to NO.

Conclusion:

Podfile introduces the flutter engine and iOS dependent libraries through a series of configuration file reads, file copies, etc. The result is something like:

target 'Runner' do pod 'Flutter', :path => 'Flutter' pod 'sqflite', : the path = > 'FLUTTER_ROOT/.pub-cache/hosted/pub.dartlang.org/sqflite-1.3.2+3/ios pod' sqflite ', : the path = > 'FLUTTER_ROOT/.pub-cache/hosted/pub.dartlang.org/shared_preferences-0.5.12+4/ios pod' sqflite ', : the path = > 'FLUTTER_ROOT/.pub-cache/hosted/pub.dartlang.org/fijkplayer-0.8.7/ios end post_install do | installer | installer.pods_project.build_configurations.each do |config| config.build_settings['ENABLE_BITCODE'] = 'NO' end endCopy the code

Tip: FMDB does not appear in the Podfile because SQflite relies on FMDB, so FMDB will be installed based on that dependency. This is the basics of CocoaPod and should be familiar to iOS developers, so I won’t cover it here.

Plguin

The entry to the APP project is an AppDelegate, inherited from Flutter. Framework’s FlutterAppDelegate.

@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
Copy the code

1. Register the plug-in

AppDelegate performed in didFinishLaunchingWithOptions GeneratedPluginRegistrant. Register (with: self) one line of code.

GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
  [FijkPlugin registerWithRegistrar:[registry registrarForPlugin:@"FijkPlugin"]];
  [FLTSharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTSharedPreferencesPlugin"]];
  [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]];
  [FLTURLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTURLLauncherPlugin"]];
}
Copy the code

Of + (void) GeneratedPluginRegistrant registerWithRegistry: (NSObject < FlutterPluginRegistry > *) registry method is to perform flutter pub Get is automatically generated by Flutter. Of course, only libraries that rely on Flutter to interact with iOS native will register the plugin.

The file can also be edited manually, but this is usually not necessary.

We take SqflitePlugin as an example to introduce the registration process of Plugin.

SqflitePlugin
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
    FlutterMethodChannel* channel = [FlutterMethodChannel
                                     methodChannelWithName:_channelName
                                     binaryMessenger:[registrar messenger]];
    SqflitePlugin* instance = [[SqflitePlugin alloc] init];
    [registrar addMethodCallDelegate:instance channel:channel];
}
Copy the code

A FlutterMethodChannel is a channel through which a Flutter can call methods to the iOS host App and get results.

The process is as follows:

The code above stands for:

  1. Create a FlutterMethodChannel named SqflitePlugin
  2. Will thischannelRegistered toFLutterEngineSo that the FLUTTER code can passFLutterEngineCall thischannelthe- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{}Methods.
FlutterEngine
- (void)addMethodCallDelegate:(NSObject<FlutterPlugin>*)delegate
                      channel:(FlutterMethodChannel*)channel {
  [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    [delegate handleMethodCall:call result:result];
  }];
}
Copy the code

2. Definition and invocation of Flutter methods

The name SqflitePlugin is defined in the pubspec.yaml file of the SQflite plugin, so the code on the Flutter side knows which MethodChannel to send messages to the iOS code through. This name corresponds to iOS.

pubspec.yaml
flutter:
  plugin:
    platforms:
      android:
        package: com.tekartik.sqflite
        pluginClass: SqflitePlugin
      ios:
        pluginClass: SqflitePlugin
      macos:
        pluginClass: SqflitePlugin
Copy the code

Sqflite defines many methods, such as the insert method. These methods are asynchronous, so the return value needs to be wrapped in a Future.

Future<int> insert(String table, Map<String, dynamic> values,
      {String nullColumnHack, ConflictAlgorithm conflictAlgorithm});
Copy the code

The Flutter code can directly call the INSERT method when manipulating the database. In this case, the FlutterEngine passes the parameters to the iOS code and waits for a step back.

2. The method on the iOS end processes the process and returns a value

Since the corresponding plugin SqflitePlugin is registered in the AppDelegate, FlutterEngine then calls the – (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result{} method.

SqflitePlugin
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { FlutterResult wrappedResult = ^(id res)  { dispatch_async(dispatch_get_main_queue(), ^{ result(res); }); }; . else if ([_methodInsert isEqualToString:call.method]) { [self handleInsertCall:call result:wrappedResult]; }... else { result(FlutterMethodNotImplemented); - (void)handleInsertCall:(FlutterMethodCall*)call result:(FlutterResult)result {SqfliteDatabase* database  = [self getDatabaseOrError:call result:result]; if (database == nil) { return; } dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [database.fmDatabaseQueue inDatabase:^(FMDatabase *db) { SqfliteMethodCallOperation* operation = [SqfliteMethodCallOperation newWithCall:call result:result]; [self insert:database fmdb:db operation:operation]; }]; }); }Copy the code

After performing the insert operation, FMDB encapsulates the result into FlutterResult, which is returned to Flutter.

FlutterAppDelegate

The main task of our AppDelegate is to register the plug-in. Allow the Flutter code to easily call Native code.

And the AppDelegate inherits from the FlutterAppDelegate, so what does the FlutterAppDelegate do?

The FlutterAppDelegate is in the Flutter. Framework, because it is packaged as a library, we can only see the header file. If we need to see the source code, we need to go into the Flutter Engine to see the source code.

FlutterAppDelegate
@implementation FlutterAppDelegate { FlutterPluginAppLifeCycleDelegate* _lifeCycleDelegate; } // Returns the key window's rootViewController, if it's a FlutterViewController. // Otherwise, returns nil. - (FlutterViewController*)rootFlutterViewController { if (_rootFlutterViewControllerGetter ! = nil) { return _rootFlutterViewControllerGetter(); } UIViewController* rootViewController = _window.rootViewController; if ([rootViewController isKindOfClass:[FlutterViewController class]]) { return (FlutterViewController*)rootViewController; } return nil; } #pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController - (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey { FlutterViewController* flutterRootViewController = [self rootFlutterViewController]; if (flutterRootViewController) { return [[flutterRootViewController pluginRegistry] registrarForPlugin:pluginKey]; } return nil; } - (BOOL)hasPlugin:(NSString*)pluginKey { FlutterViewController* flutterRootViewController = [self rootFlutterViewController]; if (flutterRootViewController) { return [[flutterRootViewController pluginRegistry] hasPlugin:pluginKey]; } return false; } - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey { FlutterViewController* flutterRootViewController = [self rootFlutterViewController]; if (flutterRootViewController) { return [[flutterRootViewController pluginRegistry] valuePublishedByPlugin:pluginKey]; } return nil; }Copy the code

The important code is explained as follows:

  1. FlutterAppDelegate a FlutterPluginAppLifeCycleDelegate _lifeCycleDelegate attribute types, its role is to distribute the App lifecycle of change. It is an important method of – (void) addDelegate: (NSObject < FlutterApplicationLifeCycleDelegate > *) delegate {}, is who wants to know the App lifecycle is added, It notifies everyone when the App’s life cycle changes.

  2. FlutterAppDelegate root view for a FlutterViewController flutterRootViewController type of object.

  3. A series of related to the Plugin code, mainly to the Plugin to register flutterRootViewController FlutterEngine object.

It is easy to understand how the AppDelegate MethodChannel connected, because flutterRootViewController loading is Flutter App compiled code.

FlutterViewController

As mentioned earlier, the root view of the FlutterAppDelegate is the FlutterViewController. How does the FlutterViewController load the Flutter App?

FlutterViewController
@implementation FlutterViewController {
  std::unique_ptr<fml::WeakPtrFactory<FlutterViewController>> _weakFactory;
  fml::scoped_nsobject<FlutterEngine> _engine;

  fml::scoped_nsobject<FlutterView> _flutterView;
  fml::scoped_nsobject<UIView> _splashScreenView;
}
Copy the code

The FlutterViewController has several important properties:

  1. _engineisFlutterEngine, responsible for rendering interaction and other functions
  2. _flutterViewIs to showFlutter ApptheView
  3. _splashScreenViewIt shows the startup diagramView

The key to the

The various constructors of FlutterViewController are finally called – (void)sharedSetupWithProject:(nullable FlutterDartProject*)project initialRoute:(nullable NSString*)initialRoute

FlutterViewController
- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project initialRoute:(nullable NSString*)initialRoute { // Need the project to get settings for the view. Initializing it here means if (! project) { project = [[[FlutterDartProject alloc] init] autorelease]; } auto engine = fml::scoped_nsobject<FlutterEngine>{[[FlutterEngine alloc] initWithName:@"io.flutter" project:project allowHeadlessExecution:self.engineAllowHeadlessExecution restorationEnabled:[self restorationIdentifier] != nil]}; _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute]; [self loadDefaultSplashScreenView]; [self performCommonViewControllerInitialization]; }Copy the code
  1. To generate aFlutterDartProjectobjectproject, this object mainly describesFlutter APPThe most important one is to find executable files.
  2. According to thisprojectThe setup information generates aFlutterEngineobjectengine.
  3. To generate aFlutterViewobject_flutterViewTo render the View.
  4. _enginefindFlutter APPExecutable file entrymain.dartStart executing and then render to_flutterViewOn.
  5. See if the startup diagram needs to be loaded
  6. Some general initialization stuff
FlutterDartProject

FlutterDartProject through FLTDefaultSettingsForBundle method can generate some general Settings.

flutter::Settings FLTDefaultSettingsForBundle(NSBundle* bundle) {
    // Frameworks directory.
    if (settings.application_library_path.size() == 0) {
      NSString* applicationFrameworkPath = [mainBundle pathForResource:@"Frameworks/App.framework"
                                                                ofType:@""];
      if (applicationFrameworkPath.length > 0) {
        NSString*  =
            [NSBundle bundleWithPath:applicationFrameworkPath].executablePath;
        if (executablePath.length > 0) {
          settings.application_library_path.push_back(executablePath.UTF8String);
        }executablePath
      }
    }
  }

  // Checks to see if the flutter assets directory is already present.
  if (settings.assets_path.size() == 0) {
    NSString* assetsName = [FlutterDartProject flutterAssetsName:bundle];
    NSString* assetsPath = [bundle pathForResource:assetsName ofType:@""];

    }
  }

  // Domain network configuration
  NSDictionary* appTransportSecurity =
      [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"];
  settings.may_insecurely_connect_to_all_domains =
      [FlutterDartProject allowsArbitraryLoads:appTransportSecurity];
  settings.domain_network_policy =
      [FlutterDartProject domainNetworkPolicy:appTransportSecurity].UTF8String;
  }
  return settings;
}

Copy the code

This code does the following:

  1. If not specified,Flutter APPThe executable file is locatedFLutterIn the directoryApp.frameworkLet’s call that oneAppThat is, all the Flutter code is packaged into one executable file.

2. Specify the path of the image. 3.