This paper aims to implement a flexible, non-invasive, low coupling iOS Flutter hybrid engineering. We want mixed development to have at least the following features:

  • No intrusion to Native engineering
  • Zero coupling to Native engineering
  • It does not affect the development process and packaging process of Native engineering
  • Easy local debugging

I. Native Flutter hybrid engineering method provided by Flutter

This article introduces how to Add a Flutter to existing apps. What follows is a step-by-step introduction

1. Create the Flutter project

Please install Flutter by using baidu /Google Flutter installation tutorial. Then go to any directory and execute the flutter create -t module my_flutter. “my_flutter” is the name of the flutter project to be created.

2. Introduce Flutter into the existing Native project through Cocoapods

Add the following code to your Podfile

flutter_application_path = "xxx/xxx/my_flutter"
eval(File.read(File.join(flutter_application_path, '.ios'.'Flutter'.'podhelper.rb')), binding)
Copy the code

Then execute pod Install

3. Modify Native project

Open the Xcode project, select the target that you want to add to the Flutter App, select Build Phases, click the + sign at the top, select New Run Script Phase, and enter the following Script

"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
Copy the code

2. Analyze Native Flutter hybrid engineering

Analyze the problems in each step according to the above three steps and provide optimization solutions.

1. Create the Flutter project

This step starts by installing Flutter on your computer and then using Flutter Create. The problem here is that everyone may have different versions of Flutter installed during team development, causing Dart layer Api compatibility or Flutter VIRTUAL machine inconsistencies. Make sure that the Flutter engineering relies on the same Flutter SDK in team collaboration, so a tool is needed to execute the Flutter instructions using the corresponding version of the Flutter SDK according to the current Flutter engineering. There is a tool called Flutter_wrapper that uses Flutterw instead of the flutter instructions. The tool will automatically place the FLUTTER SDK in the current flutter project directory and execute the flutter commands in the current flutter project. This eliminates reliance on the Local PC install of the Flutter SDK.

Flutter_wrapper use:

  1. flutter createCreate the Flutter project using the native Flutter SDK
  2. Go to the Flutter project directory and install ‘flutter_wrapper’sh -c "$(curl -fsSL https://raw.githubusercontent.com/passsy/flutter_wrapper/master/install.sh)"
  3. Thereafter the current Flutter project needs to be usedflutterUse it wherever you command./flutterwTo take the place of

2. Introduce Flutter into the existing Native project through Cocoapods

This step adds a ‘PodHelper. rb’ Ruby script to your Podfile, which is executed when pod install/update is installed. The script does four things:

  1. The Generated. Xcconfig file is in ‘my_flutter/.ios/Flutter/’. The file contains the Flutter SDK path, the Flutter project path, the Flutter project entry, and the build directory.
  2. Add the Flutter SDK toFlutter.frameworkAdd to Native project via POD.
  3. Add the plugin that the Flutter project depends on via POD to the Native project because some of the plugin’s have Native code.
  4. usepost_installTo close Native project bitcode, add ‘Generated. Xcconfig ‘file to Native project.

The problem with this step is that the ‘podhelper.rb’ script is read from a local Flutter project path’ flutter_application_path’. It is difficult to ensure that everyone’s local Flutter project path is the same in team collaboration. You might have to change the ‘Flutter_application_PATH’ variable frequently while synchronizing your code, which is not very friendly.

The solution to this problem is to place the Flutter project in the current Native project directory. We can add a Ruby script that pulls a copy of the Flutter project from Git and places it in the current folder every time pod install/update is executed. Thus the path of the Flutter project is unified. The general code is as follows:

flutter_application_path = __dir__ + "/.flutter/app"
`git clone git://xxxx/my_flutter.git #{flutter_application_path}`
If you want to debug the local Flutter project, open the following comment
# flutter_application_path = "xxx/xxx/my_flutter"
eval(File.read(File.join(flutter_application_path, '.ios'.'Flutter'.'podhelper.rb')), binding)
Copy the code

The above code is only temporary. To demonstrate the idea of placing the Flutter project in the current directory, there will be a complete implementation code later.

3. Modify Native project

Xcode_backend. sh: embed embed xcode_backend.sh: build embed xcode_backend.sh: build embed

  • Build: Builds the Flutter project according to the current Xcode project’s ‘configuration’ and other build configurations. ‘Configuration’ is usually ‘debug’ or ‘release’.
  • Embed: Puts the built framework and resource packs into the Xcode build directory and signs the framework

The problem here is that the Flutter engineering relies on Native engineering for compilation and affects the development and packaging process of Native engineering.

Usually ‘configuration’ has more than ‘debug’ or ‘release’, it may have a custom name, if we compile with the custom ‘configuration’ then xcode_backend.sh build will fail. Because the compilation mode of Flutter is obtained through ‘configuration’, Flutter supports Debug, Profil and Release compilation modes, and our custom name is not one of these three modes. Therefore, Flutter does not know how to compile.

Every time a Flutter is compiled at Native, it needs to be compiled. In fact, this creates interdependence: the Flutter compilation depends on the Native compilation environment, and the Native compilation depends on the Flutter compilation to pass.

We want to do this: Native relies on the compilation of Flutter and retains the ability to debug against the Flutter source.

To achieve this goal we need two parts:

  • Part 1: Create a package script for the Flutter project, which can generate the Flutter project products with one click.
  • Part 2: Obtain the compilation products of FLutter engineering in Native engineering and add them to the project via POD; And retain the ability to rely on the Flutter project source code.

Iii. Realize Native Flutter hybrid engineering

Let’s implement the two parts mentioned above

Part 1 Implementing the “Package Script”

In this part, we need to implement the script to automatically package the Flutter project. The script process is divided into the following steps:

  1. Check the Flutter environment and pull the Flutter Plugin
  2. Build Flutter project artifacts
  3. Copy the Native code in the Flutter plugin
  4. Synchronize artifacts to the server where the artifacts are published

Let’s take a step by step analysis and implement each step:

(1) Check the Flutter environment and pull the Flutter Plugin

This step checks if ‘flutter_wrapper’ is installed, installs it if so, and then executes./ Flutterw Packages get with the Shell code as follows:

flutter_get_packages() { echo "=================================" echo "Start get flutter app plugin" local flutter_wrapper="./flutterw" if [ -e $flutter_wrapper ]; then echo 'flutterw installed' >/dev/null else bash -c "$(curl -fsSL https://raw.githubusercontent.com/passsy/flutter_wrapper/master/install.sh)" if [[ $? -ne 0 ]]; Echo "Failed to install flutter_wrapper." exit -1 fi fi ${flutter_wrapper} packages get --verbose if [[ $? -ne 0 ]]; Echo "Failed to install flutter plugins." exit -1 fi echo "Finish get flutter app plugin"}Copy the code

(2) Compile Flutter engineering products

This step is the core of the script. The main logic is similar to ‘xcode_backend.sh build’ above.

#Default debug compilation mode
BUILD_MODE="debug"
#Compiled for CPU platform
ARCHS_ARM="arm64,armv7"
#Flutter SDK path
FLUTTER_ROOT=".flutter"
#Compile the directories
BUILD_PATH=".build_ios/${BUILD_MODE}"
#A directory where products are stored
PRODUCT_PATH="${BUILD_PATH}/product"
#The directory that contains the compiled Flutter frameworkPRODUCT_APP_PATH="${PRODUCT_PATH}/Flutter" build_flutter_app() { echo "=================================" echo "Start Dart file local target_path="lib/main.dart" # Build flutter app" # create directory mkdir -p -- "${PRODUCT_APP_PATH}" # Local artifact_variant="unknown" case "$BUILD_MODE" in release*) artifact_variant="ios-release"; profile*) artifact_variant="ios-profile" ;; debug*) artifact_variant="ios" ;; *) echo "ERROR: Unknown FLUTTER_BUILD_MODE: ${BUILD_MODE}." exit -1 ;; esac if [[ "${BUILD_MODE}" != "debug" ]]; Build the fLutter app output App.framework ${FLUTTER_ROOT}/bin/flutter --suppress-analytics \ --verbose \ build aot \ --output-dir="${BUILD_PATH}" \ --target-platform=ios \ --target="${target_path}" \ --${BUILD_MODE} \ --ios-arch="${ARCHS_ARM}" if [[ $? -ne 0 ]]; Then echo "Failed to build flutter app" exit -1 fi else # debug Because the flutter code is not compiled into binary machine code in debug mode, it is packaged into resource bundles during the subsequent build bundle, # in the 'xcode_backend.sh' script, this step is only compiled into an App.  Xcode_backend. sh is executed with Xcode, so it can be used to compile 'app. framework' correctly. Even copying the code for 'xcode_backend.sh' will not compile 'app. framework' properly unless the compilation environment is configured correctly. # # And I didn't want to go to the trouble, so I chose a different path: # create a Flutter project, # compile and run it on the simulator in debug mode to get the App. Framework x86_64, # run it on the real machine. App.framework x86_64/arm64/armv7 app. framework x86_64/arm64/armv7 app. framework The resulting App. Framework can be used on both the simulator and the real machine. So other flutter engineering with local app_framework_debug = "iOSApp/Debug/App. Framework" cp -r - "${app_framework_debug}" "${BUILD_PATH}" fi # copy info.plist to App.framework app_plist_path=".ios/Flutter/AppFrameworkInfo.plist" cp -- "${app_plist_path}" "${BUILD_PATH}/App.framework/Info.plist" local framework_path="${FLUTTER_ROOT}/bin/cache/artifacts/engine/${artifact_variant}" local flutter_framework="${framework_path}/Flutter.framework" local flutter_podspec="${framework_path}/Flutter.podspec" # copy  framework to PRODUCT_APP_PATH cp -r -- "${BUILD_PATH}/App.framework" "${PRODUCT_APP_PATH}" cp -r -- "${flutter_framework}" "${PRODUCT_APP_PATH}" cp -r -- "${flutter_podspec}" "${PRODUCT_APP_PATH}" local precompilation_flag="" if [[ "$BUILD_MODE" != "debug" ]]; then precompilation_flag="--precompiled" fi # build bundle ${FLUTTER_ROOT}/bin/flutter --suppress-analytics \ --verbose \ build bundle \ --target-platform=ios \ --target="${target_path}" \ --${BUILD_MODE} \ --depfile="${BUILD_PATH}/snapshot_blob.bin.d" \ --asset-dir="${BUILD_PATH}/flutter_assets" \ ${precompilation_flag} if [[ $? -ne 0 ]]; then echo "Failed to build flutter assets" exit -1 fi # copy Assets cp -rf -- "${BUILD_PATH}/flutter_assets" "${PRODUCT_APP_PATH}/App.framework" # setting podspec # replace: # 'Flutter.framework' # to: # 'Flutter.framework', 'App.framework' sed -i '' -e $'s/\'Flutter.framework\'/\'Flutter.framework\', \'App.framework\'/g' ${PRODUCT_APP_PATH}/Flutter.podspec echo "Finish build flutter app" }Copy the code

(3) Copy the Native code of the Flutter plugin

The various plug-ins used for Flutter may contain Native code that already provides podspecs that can be imported directly using POD. All we need to do is copy the Native code of the plugin into the production directory. Flutter creates a pod library for Native plugin ‘FlutterPluginRegistrant’. This also needs to be copied. There is a.flutter-plugins file in the root directory of the Flutter project. Pugin_name =/xx/xx/xx

flutter_copy_packages() {
    echo "================================="
    echo "Start copy flutter app plugin"

    local flutter_plugin_registrant="FlutterPluginRegistrant"
    local flutter_plugin_registrant_path=".ios/Flutter/${flutter_plugin_registrant}"
    echo "copy 'flutter_plugin_registrant' from '${flutter_plugin_registrant_path}' to '${PRODUCT_PATH}/${flutter_plugin_registrant}'"
    cp -rf -- "${flutter_plugin_registrant_path}" "${PRODUCT_PATH}/${flutter_plugin_registrant}"

    local flutter_plugin=".flutter-plugins"
    if [ -e $flutter_plugin ]; then
        OLD_IFS="$IFS"
        IFS="="
        cat ${flutter_plugin} | while read plugin; do
            local plugin_info=($plugin)
            local plugin_name=${plugin_info[0]}
            local plugin_path=${plugin_info[1]}

            if [ -e ${plugin_path} ]; then
                local plugin_path_ios="${plugin_path}ios"
                if [ -e ${plugin_path_ios} ]; then
                    if [ -s ${plugin_path_ios} ]; then
                        echo "copy plugin 'plugin_name' from '${plugin_path_ios}' to '${PRODUCT_PATH}/${plugin_name}'"
                        cp -rf ${plugin_path_ios} "${PRODUCT_PATH}/${plugin_name}"
                    fi
                fi
            fi
        done
        IFS="$OLD_IFS"
    fi

    echo "Finish copy flutter app plugin"
}
Copy the code

(4) Synchronize the product to the server that retains the product

The above steps will result in a production directory with several secondary directories, each containing a PodSpec file.

Copy the directory to Native projects and reference it as pod ‘pod_name’, :path=>’xx/ XXX ‘.

After we have the product, we need a place to store the product, you can go to this place to download, this step is more flexible, you can choose to put the product in git repository, HTTP server, FTP server, etc. I finally chose to zip the product and upload it to Maven. The reason is to keep it in the same place as the Android Flutter product and Maven has done a good job of product versioning.

The Maven upload code is relatively simple and will not be described here. If you are interested, check out the github repository at the end of this article.

The Flutter project version Settings are ‘pubspec.yaml’ files in the project directory. This file is read by the packaging script to determine the product version.

H -m release./build_ios.h -m release. What is not mentioned above is that only packages compiled in release mode are uploaded to the server.

The second step is that Native relies on Flutter products

In this part, we need to obtain the specified version of The Flutter project release and integrate it into the Native project, while retaining the ability to debug the Flutter project.

Let’s also break up the script flow:

  • Obtain Flutter engineering products
    • Get the release
    • Get debug products
  • Introduce Flutter engineering products through POD

(1) Obtain Flutter engineering products

Only releases are placed on the production server. Debug only compiles to the production directory. The reason for not uploading debug is that the debug phase is the development phase. For example, it is not appropriate to upload a package to the App Store during the development phase. This means that the release and debug fetch logic is different, and our script supports switching between the two methods, so add the following code to the Podfile:

Set the version of the Flutter app to be introduced
FLUTTER_APP_VERSION=1.1.1 ""

Whether to debug the Flutter app
When # is true, the FLUTTER_APP_VERSION configuration is invalid and the following three configurations take effect
When # is false, the FLUTTER_APP_VERSION configuration takes effect, and the following three configurations are invalid
FLUTTER_DEBUG_APP=false

Git. The contents of a Flutter App are placed in the current project directory. Flutter/App
# If FLUTTER_APP_PATH is specified, this configuration is invalid
FLUTTER_APP_URL="git:/xxxx.git"
# flutter git branch, default to master
# If FLUTTER_APP_PATH is specified, this configuration is invalid
FLUTTER_APP_BRANCH="master"

Git configuration is invalid if there is a value for flutter local project directory, absolute or relative
FLUTTER_APP_PATH=".. /my_flutter"

eval(File.read(File.join(__dir__.'flutterhelper.rb')), binding)
Copy the code

The last line of code reads and executes the “flutterhelper.rb” file. The “flutterhelper.rb” file will fetch the global variables defined above. Depending on these variables, the following code is used to select release or debug:

if FLUTTER_DEBUG_APP.nil? || FLUTTER_DEBUG_APP == false
    Use the Flutter release mode
    puts "Start installing release Mode Flutter app"
    install_release_flutter_app()
else
    Flutter debug is configured, use flutter debug mode
    puts "Start installing debug Mode Flutter APP"
    install_debug_flutter_app()
end
Copy the code

Install_release_flutter_app is the function that operates on the release product, and install_debug_flutter_app is the function that operates on the debug product.

Handling the release mode is mainly to get the release product, the code is as follows:

Install the official environment app
def install_release_flutter_app
    if FLUTTER_APP_VERSION.nil?
        raise "Error: Please set the version of Flutter app to install in your Podfile, e.g. FLUTTER_APP_VERSION='1.0.0'"
    else
        puts "The currently installed version of The Flutter app is#{FLUTTER_APP_VERSION}"
    end

    # Directory to store products
    flutter_release_path = File.expand_path('.flutter_release')
    # Whether the current version of the artifact already exists
    has_version_file = true
    if! File.exist? flutter_release_path FileUtils.mkdir_p(flutter_release_path) has_version_file =false
    end

    The directory where the current version of the product is stored
    flutter_release_version_path = File.join(flutter_release_path, FLUTTER_APP_VERSION)
    if! File.exist? flutter_release_version_path FileUtils.mkdir_p(flutter_release_version_path) has_version_file =false
    end

    # product package
    flutter_package = "flutter.zip"
    flutter_release_zip_file =  File.join(flutter_release_version_path, flutter_package)
    if! File.exist? flutter_release_zip_file has_version_file =false
    end

    # Product pack download completion mark
    flutter_package_downloaded = File.join(flutter_release_version_path, "download.ok")
    if! File.exist? flutter_package_downloaded has_version_file =false
    end

    if has_version_file == true
        # decompression
        flutter_package_path = unzip_release_flutter_app(flutter_release_version_path, flutter_release_zip_file)
        # Start installation
        install_release_flutter_app_pod(flutter_package_path)
    else
        Delete old files
        FileUtils.rm_rf(flutter_release_zip_file)
        # delete marker
        FileUtils.rm_rf(flutter_package_downloaded)

        # download
        download_release_flutter_app(FLUTTER_APP_VERSION, flutter_release_zip_file, flutter_package_downloaded)
        # decompression
        flutter_package_path = unzip_release_flutter_app(flutter_release_version_path, flutter_release_zip_file)
        # Start installation
        install_release_flutter_app_pod(flutter_package_path)
    end
end
Copy the code

Unzip_release_flutter_app unzip_release_flutter_app unzip_release_flutter_app unzip_release_flutter_app unzip_release_flutter_app unzip_release_flutter_app unzip_release_flutter_app unzip_release_flutter_app unzip_release_flutter_app Install_release_flutter_app_pod is a function that adds artifacts to Native via POD, more on this later.

To handle the debug mode, obtain the Flutter project source code and run build_ios.sh -m debug to package it. The debug product directory is as follows:

Install the development environment app
def install_debug_flutter_app

    puts "This process may be slower if you are running the Development environment Flutter project for the first time."
    puts "Please be patient for ☕️️ ☕ ☕️\n"
    
    # Default Flutter App directory
    flutter_application_path = __dir__ + "/.flutter/app"
    flutter_application_url = ""
    flutter_application_branch = 'master'
    
    # if you specify FLUTTER_APP_PATH, use native code, copy to pull from git
    ifFLUTTER_APP_PATH ! =nil
        File.expand_path(FLUTTER_APP_PATH)
        ifFile.exist? (FLUTTER_APP_PATH) flutter_application_path = FLUTTER_APP_PATHelse
            flutter_application_path = File.expand_path(FLUTTER_APP_PATH)
            if! File.exist? (flutter_application_path) raise"Error: #{FLUTTER_APP_PATH}Address does not exist!"
            end
        end
        
        puts "\nFlutter App path:"+flutter_application_path
    else
        ifFLUTTER_APP_URL ! =nil
            flutter_application_url = FLUTTER_APP_URL
            ifFLUTTER_APP_BRANCH ! =nil
                flutter_application_branch = FLUTTER_APP_BRANCH
            end
        else
            raise "Error: Add the Flutter App git address configuration to 'Podfile' in 'flutterhelper.rb' file"
        end
        puts "\n Pull Flutter App code"
        puts "Flutter App path:"+flutter_application_path
        update_flutter_app(flutter_application_path, flutter_application_url, flutter_application_branch)
    end

    puts "\n Compile Flutter App"
    To speed things up, use a domestic mirror address
    `export PUB_HOSTED_URL=https://pub.flutter-io.cn && \ export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn && \  cd#{flutter_application_path} && \
    #{flutter_application_path}/build_ios.sh -m debug`

    if$? .to_i ==0
        flutter_package_path = "#{flutter_application_path}/.build_ios/debug/product"
        # Start installation
        install_release_flutter_app_pod(flutter_package_path)
    else
        raise "Error: Failed to compile Flutter App"
    end
end
Copy the code

Update_flutter_app is a function that pulls code from Git. See github repository at the end of this article.

(2) Introduce Flutter engineering products through POD

After the above two functions are executed, the directory where the product is stored is obtained. The following is just needed to import it into the Native repository, that is, the install_release_flutter_app_pod function. The code is as follows:

Install the Flutter app via POD
def install_release_flutter_app_pod(product_path)
    if product_path.nil?
        raise "Error: Invalid flutter app directory"
    end

    puts "Import the Flutter APP into the project via POD"

    Dir.foreach product_path do |sub|
        ifsub.eql? ('. ') ||sub.eql? ('.. ') 
            next
        end

        sub_abs_path = File.join(product_path, sub)
        pod sub, :path=>sub_abs_path
    end

    post_install do |installer|
        installer.pods_project.targets.each do |target|
            target.build_configurations.each do |config|
                config.build_settings['ENABLE_BITCODE'] = 'NO'
            end
        end
    end
end 
Copy the code

If you want to change the release product version, set FLUTTER_APP_VERSION. If you want to debug flutter set FLUTTER_DEBUG_APP=true and if you debug native code set FLUTTER_APP_PATH=”.. /my_flutter”, comment out FLUTTER_APP_PATH and configure FLUTTER_APP_URL FLUTTER_APP_BRANCH.

Four,

According to the requirements of the mixed engineering mentioned above, the following conclusions can be made:

  • The Flutter project does not rely on Native projects at all. Instead, it is compiled and packaged using the ‘build_ios.sh’ script.
  • Introduction of the Flutter project through POD does not immerse the Native project. Do not add the Flutter packaging script to the Native project.
  • Native developers just need to executepod installAll Flutter dependencies are added to the project without the engineer having to configure the Flutter development environment; It does not affect Native packaging;
  • Also retains the ability to debug the Flutter project locally;

Github repository: iOS_Flutter_Hybrid_Project