美文网首页Android开发经验谈傲视苍穹iOS《Objective-C》VIP专题Android开发
GitHub标星8.3K,字节跳动大牛在原生项目中集成Flutt

GitHub标星8.3K,字节跳动大牛在原生项目中集成Flutt

作者: 进阶程序员007 | 来源:发表于2020-09-11 15:53 被阅读0次

    概述

    使用Flutter从零开始开发App是一件轻松惬意的事情,但对于一些成熟的产品来说,完全摒弃原有App的历史沉淀,全面转向Flutter是不现实的。因此使用Flutter去统一Android、iOS技术栈,把它作为已有原生App的扩展能力,通过有序推进来提升移动终端的开发效率。
    目前,想要在已有的原生App里嵌入一些Flutter页面主要有两种方案。一种是将原生工程作为Flutter工程的子工程,由Flutter进行统一管理,这种模式称为统一管理模式。另一种是将Flutter工程作为原生工程的子模块,维持原有的原生工程管理方式不变,这种模式被称为三端分离模式,如下图所示。
    [图片上传失败...(image-a3088f-1599809816612)]
    三端代码分离模式的原理是把Flutter模块作为原生工程的子模块,从而快速地接入Flutter模块,降低原生工程的改造成本。在Flutter 1.1x时代,在原生已有app中接入Flutter的步骤比较繁琐,具体可以可以参考:Flutter与原生混合开发
    不过,从Flutter 1.20.x版本开始,Flutter对原生app接入Flutter进行了优化和升级,下面是具体介绍。

    原生Android集成Flutter

    支持的特性

    • 在 Gradle 脚本中添加一个自动构建并引入 Flutter 模块的 Flutter SDK 钩子。
    • 将 Flutter 模块构建为通用的 Android Archive (AAR) 以便集成到您自己的构建系统中,并提高 Jetifier 与 AndroidX 的互操作性;
    • FlutterEngine API 用于启动并持续地为挂载 FlutterActivity 或 FlutterFragment 提供独立的 Flutter 环境;
    • Android Studio 的 Android 与 Flutter 同时编辑,以及 Flutter module 创建与导入向导;
    • 支持Java 和 Kotlin 为宿主的应用程序;

    集成Flutter

    首先,我们来看一下最终的效果,如下图所示。
    [图片上传失败...(image-7f92db-1599809816612)]

    集成Flutter主要有两种方式,一种是使用Android Studio工具的方式,另一种是使用手动的方式。

    使用Android Studio方式

    直接使用 Android Studio 是在现有应用中自动集成 Flutter 模块比较便捷的方法。在 Android Studio 打开现有的 Android 原生项目,然后依次点击菜单按钮 File > New > New Module…创建出一个可以集成的新 Flutter 模块,或者选择导入已有的 Flutter 模块,如下图所示。
    [图片上传失败...(image-ba95d-1599809816612)]
    选择Module的类型为Flutter Module,然后在向导窗口中填写模块名称、路径等信息,如下图所示。
    [图片上传失败...(image-70a783-1599809816612)]

    此时,Android Studio 插件就会自动为这个 Android 项目配置添加 Flutter 模块作为依赖项,这时集成应用就已准备好进行下一步的构建。

    手动集成

    如果想要在不使用 Flutter 的 Android Studio 插件的情况下手动将 Flutter 模块与现有的 Android 应用集成,可以使用下面的步骤。

    假设我们的原生应用在 some/path/MyApp 路径下,那么在Flutter 项目的同级目录下新建一个Flutter模块,命令如下。

    cd some/path/
    flutter create -t module --org com.example my_flutter
    
    

    完成上面的命令后,会在 some/path/my_flutter/ 目录下创建一个 Flutter 模块项目。该模块项目会包含一些 Dart 代码和一些一个隐藏的子文件夹 .android/,.android 文件夹包含一个 Android 项目,该项目不仅可以帮助你通过 flutter run 运行这个 Flutter 模块的独立应用,而且还可以作为封装程序来帮助引导 Flutter 模块作为可嵌入的 Android 库。

    同时,由于Flutter Android 引擎需要使用到 Java 8 中的新特性。因此,需要在宿主 Android 应用的 build.gradle 文件的 android { } 块中声明了以下源兼容性代码。

    android {
      //...
      compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
      }
    }
    
    

    接下来,需要将Flutter module添加到原生Android工程的依赖中。将 Flutter 模块添加到原生Android应用程序中主要有两种方法实现。使用AAR包方式和直接使用module源码的方式。使用AAR包方式需要先将Flutter 模块打包成AAR包。假设,你的 Flutter 模块在 some/path/my_flutter 目录下,那么打包AAR包的命令如下。

    cd some/path/my_flutter
    flutter build aar
    
    

    然后,根据屏幕上的提示完成集成操作,如下图所示,当然也可以在Android原生工程中进行手动添加依赖代码。


    事实上,该命令主要用于创建(默认情况下创建 debug/profile/release 所有模式)本地存储库,主要包含以下文件,如下所示。

    build/host/outputs/repo
    └── com
        └── example
            └── my_flutter
                ├── flutter_release
                │   ├── 1.0
                │   │   ├── flutter_release-1.0.aar
                │   │   ├── flutter_release-1.0.aar.md5
                │   │   ├── flutter_release-1.0.aar.sha1
                │   │   ├── flutter_release-1.0.pom
                │   │   ├── flutter_release-1.0.pom.md5
                │   │   └── flutter_release-1.0.pom.sha1
                │   ├── maven-metadata.xml
                │   ├── maven-metadata.xml.md5
                │   └── maven-metadata.xml.sha1
                ├── flutter_profile
                │   ├── ...
                └── flutter_debug
                    └── ...
    
    

    可以发现,使用上面的命令编译的AAR包主要分为debug、profile和release三个版本,使用哪个版本的AAR需要根据原生的环境进行选择。找到AAR包,然后再Android宿主应用程序中修改 app/build.gradle 文件,使其包含本地存储库和上述依赖项,如下所示。

    android {
      // ...
    }
    
    repositories {
      maven {
        url 'some/path/my_flutter/build/host/outputs/repo'
        // This is relative to the location of the build.gradle file
        // if using a relative path.
      }
      maven {
        url 'https://storage.googleapis.com/download.flutter.io'
      }
    }
    
    dependencies {
      // ...
      debugImplementation 'com.example.flutter_module:flutter_debug:1.0'
      profileImplementation 'com.example.flutter_module:flutter_profile:1.0'
      releaseImplementation 'com.example.flutter_module:flutter_release:1.0'
    }
    
    

    当然,除了命令方式外,还可以使用Android Studio来构建AAR包。依次点击 Android Studio 菜单中的 Build > Flutter > Build AAR 即可构建Flutter 模块的 AAR包,如下图所示。


    除了AAR包方式外,另一种方式就是使用源码的方式进行依赖,即将flutter_module模块作为一个模块添加到Android原生工程中。首先,将Flutter 模块作为子项目添加到宿主应用的 settings.gradle 中,如下所示。

    // Include the host app project.
    include ':app'                                   
    setBinding(new Binding([gradle: this]))                              
    evaluate(new File(                                                      
      settingsDir.parentFile,                                           
      'my_flutter/.android/include_flutter.groovy'                       
    ))                                                                   
    
    

    binding 和 evaluation 脚本可以使 Flutter 模块将其自身(如 :flutter)和该模块使用的所有 Flutter 插件(如 :package_info,:video_player 等)都包含在 settings.gradle 上下文中,然后在原生Android工程的app目录下的build.gradle文件下添加如下依赖代码。

    dependencies {
      implementation project(':flutter')
    }
    
    

    到此,在原生Android工程中集成Flutter环境就完成了,接下来编写代码即可。

    添加Flutter页面

    正常跳转

    1, 添加FlutterActivity
    Flutter提供了一个FlutterActivity来作为Flutter的容器页面,FlutterActivity和Android原生的Activity没有任何区别,可以认为它是Flutter的父容器组件,但在原生Android程序中,它就是一个普通的Activity,这个Activity必须在AndroidManifest.xml中进行注册,如下所示。

    <activity
      android:name="io.flutter.embedding.android.FlutterActivity"
      android:theme="@style/LaunchTheme"
      android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
      android:hardwareAccelerated="true"
      android:windowSoftInputMode="adjustResize" />
    
    

    对于theme属性,我们可以使用Android的其他样式进行替换,此主题样式会决定了应用的系统样式。

    2,打开FlutterActivity

    在AndroidManifest.xml中注册FlutterActivity后,然后我们可以在任何地方启动这个FlutterActivity,如下所示。

    myButton.setOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        startActivity(
          FlutterActivity.createDefaultIntent(MainActivity.this)
        );
      }
    });
    
    

    运行上面的代码,发现并不会跳转到Flutter页面,因为我们并没有提供跳转的地址。下面的示例将演示如何使用自定义路由跳转到Flutter模块页面中,如下所示。

    myButton.addOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        startActivity(
          FlutterActivity
            .withNewEngine()
            .initialRoute("/my_route")
            .build(currentActivity)
          );
      }
    });
    
    

    其中,my_route为Flutter模块的初始路由,关于Flutter的路由知识,可以看下面的文章:Flutter开发之路由与导航

    我们使用withNewEngine()工厂方法配置,创建一个的FlutterEngine实例。当运行上面的代码时,应用就会由原生页面跳转到Flutter模块页面。

    3,使用带有缓存的FlutterEngine

    每个FlutterActivity在默认情况下都会创建自己的FlutterEngine,并且每个FlutterEngine在启动时都需要有一定的预热时间。这意味着在原生页面跳转到Flutter模块页面之前会一定的时间延迟。为了尽量减少这个延迟,你可以在启动Flutter页面之前先预热的FlutterEngine。即在应用程序中运行过程中找一个合理的时间实例化一个FlutterEngine,如在Application中进行初始化,如下所示。

    public class MyApplication extends Application {
      @Override
      public void onCreate() {
        super.onCreate();
        flutterEngine = new FlutterEngine(this);
    
        flutterEngine.getDartExecutor().executeDartEntrypoint(
          DartEntrypoint.createDefault()
        );
    
        FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);
      }
    }
    
    

    其中,FlutterEngineCache的ID可以是任意的字符串,使用时请确保传递给任何使用缓存的FlutterEngine的FlutterFragment或FlutterActivity使用的是相同的ID。完成上面的自定义Application后,我们还需要在原生Android工程的AndroidManifest.xml中使用自定义的Application,如下所示。

    <application
            android:name="MyApplication"
            android:theme="@style/AppTheme">
    </application>
    

    下面我们来看一下如何在FlutterActivity页面中使用缓存的FlutterEngine,现在使用FlutterActivity跳转到Flutter模块时需要使用上面的ID,如下所示。

    myButton.addOnClickListener(new OnClickListener() {
      @Override
      public void onClick(View v) {
        startActivity(
          FlutterActivity
            .withCachedEngine("my_engine_id")
            .build(currentActivity)
          );
      }
    });
    

    可以发现,在使用withCachedEngine()工厂方法后,打开Flutter模块的延迟时间大大降低了。

    4,使用缓存引擎的初始路由
    当使用带有FlutterEngine配置的FlutterActivity或者FlutterFragment时,会有初始路由的概念,我们可以在代码中添加跳转到Flutter模块的初始路由。然而,当我们使用带有缓存的FlutterEngine时,FlutterActivity和FlutterFragment并没有提供初始路由的概念。如果开发人员希望使用带有缓存的FlutterEngine时也能自定义初始路由,那么可以在执行Dart入口点之前配置他们的缓存FlutterEngine以使用自定义初始路由,如下所示。

    public class MyApplication extends Application {
      @Override
      public void onCreate() {
        super.onCreate();
        flutterEngine = new FlutterEngine(this);
        flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
        flutterEngine.getDartExecutor().executeDartEntrypoint(
          DartEntrypoint.createDefault()
        );
    
        FlutterEngineCache
          .getInstance()
          .put("my_engine_id", flutterEngine);
      }
    }
    

    带有背景样式的跳转

    如果要修改跳转的样式,那么可以在原生Android端自定义一个主题样式呈现一个半透明的背景。首先打开res/values/styles.xml文件,然后添加自定义的主题,如下所示。

    <style name="MyTheme" parent="@style/AppTheme">
            <item name="android:windowIsTranslucent">true</item>
        </style>
    

    然后,将FlutterActivity的主题改为我们自定义的主题,如下所示。

    <activity
      android:name="io.flutter.embedding.android.FlutterActivity"
      android:theme="@style/MyTheme"
      android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
      android:hardwareAccelerated="true"
      android:windowSoftInputMode="adjustResize"
      />
    

    然后,就可以使用透明背景启动FlutterActivity,如下所示。

    // Using a new FlutterEngine.
    startActivity(
      FlutterActivity.withNewEngine()
        .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
        .build(context)
    );
    
    // Using a cached FlutterEngine.
    startActivity(
      FlutterActivity.withCachedEngine("my_engine_id")
        .backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent)
        .build(context)
    );
    
    

    添加FlutterFragment

    在Android开发中,除了Activity之外,还可以使用Fragment来加载页面,Fragment比Activity的粒度更小,有碎片化的意思。如果有碎片化加载的场景,那么可以使用FlutterFragment 。FlutterFragment允许开发者控制以下操作:

    • 初始化Flutter的路由;
    • Dart的初始页面的飞入样式;
    • 设置不透明和半透明背景;
    • FlutterFragment是否可以控制Activity;
    • FlutterEngine或者带有缓存的FlutterEngine是否能使用;

    1,将FlutterFragment 添加到Activity
    使用FlutterFragment要做的第一件事就是将其添加到宿主Activity中。为了给宿主Activity添加一个FlutterFragment,需要在Activity的onCreate()中实例化并附加一个FlutterFragment的实例,这和原生Android的Fragment使用方法是一样的,代码如下:

    public class MyActivity extends FragmentActivity {
    
        private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";
        private FlutterFragment flutterFragment;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.my_activity_layout);
            FragmentManager fragmentManager = getSupportFragmentManager();
            flutterFragment = (FlutterFragment) fragmentManager
                .findFragmentByTag(TAG_FLUTTER_FRAGMENT);
    
            if (flutterFragment == null) {
                flutterFragment = FlutterFragment.createDefault();
                fragmentManager
                    .beginTransaction()
                    .add( R.id.fragment_container, flutterFragment, TAG_FLUTTER_FRAGMENT )
                    .commit();
            }
        }
    }
    
    

    其中,代码中用到的原生Fragment的布局代码如下所示。

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
        <FrameLayout
            android:id="@+id/fragment_container"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    然后,将原生Android的启动页面改为我们的MyActivity即可。除此之外,我们还可以借助FlutterFragment来获取原生代码的生命周期,并作出相关的逻辑操作,如下所示。

    public class MyActivity extends FragmentActivity {
        @Override
        public void onPostResume() {
            super.onPostResume();
            flutterFragment.onPostResume();
        }
    
        @Override
        protected void onNewIntent(@NonNull Intent intent) {
            flutterFragment.onNewIntent(intent);
        }
    
        @Override
        public void onBackPressed() {
            flutterFragment.onBackPressed();
        }
    
        @Override
        public void onRequestPermissionsResult(
            int requestCode,
            @NonNull String[] permissions,
            @NonNull int[] grantResults
        ) {
            flutterFragment.onRequestPermissionsResult(
                requestCode,
                permissions,
                grantResults
            );
        }
    
        @Override
        public void onUserLeaveHint() {
            flutterFragment.onUserLeaveHint();
        }
    
        @Override
        public void onTrimMemory(int level) {
            super.onTrimMemory(level);
            flutterFragment.onTrimMemory(level);
        }
    }
    

    不过,上面的示例启动时使用了一个新的FlutterEngine,因此启动后会需要一定的初始化时间,导致应用启动后会有一个空白的UI,直到FlutterEngine初始化成功后Flutter模块的首页渲染完成。对于这种现象,我们同样可以在提前初始化FlutterEngine,即在应用程序的Application中初始化FlutterFragment,如下所示。

    public class MyApplication extends Application {
    
        FlutterEngine flutterEngine=null;
    
        @Override
        public void onCreate() {
            super.onCreate();
            flutterEngine = new FlutterEngine(this);
            flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
            flutterEngine.getDartExecutor().executeDartEntrypoint(
                    DartExecutor.DartEntrypoint.createDefault()
            );
            FlutterEngineCache
                    .getInstance()
                    .put("my_engine_id", flutterEngine);
        }
    }
    

    在上面的代码中,通过设置导航通道的初始路由,然后关联的FlutterEngine在初始执行runApp() ,在初始执行runApp()后再改变导航通道的初始路由属性是没有效果的。然后,我们修改MyFlutterFragmentActivity类的代码,并使用FlutterFragment.withNewEngine()使用缓存的FlutterEngine,如下所示。

    public class MyFlutterFragmentActivity extends FragmentActivity {
    
        private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";
        private FlutterFragment flutterFragment = null;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.flutter_fragment_activity);
            FragmentManager fragmentManager = getSupportFragmentManager();
            if (flutterFragment == null) {
                flutterFragment=FlutterFragment.withNewEngine()
                        .initialRoute("/")
                        .build();
    
                fragmentManager
                        .beginTransaction()
                        .add(R.id.fragment_container, flutterFragment,TAG_FLUTTER_FRAGMENT)
                        .commit();
            }
        }
    }
    

    控制FlutterFragment的渲染模式

    FlutterFragment默认使用SurfaceView来渲染它的Flutter内容,除此之外,还可以使用TextureView来渲染界面,不过SurfaceView的性能比TextureView好得多。但是,SurfaceView不能交错在Android视图层次结构中使用。此外,在Android N之前的Android版本中,SurfaceViews不能动画化,因为它们的布局和渲染不能与其他视图层次结构同步,此时,你需要使用TextureView而不是SurfaceView,使用 TextureView来渲染FlutterFragment的代码如下。

    // With a new FlutterEngine.
    FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
        .renderMode(FlutterView.RenderMode.texture)
        .build();
    
    // With a cached FlutterEngine.
    FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
        .renderMode(FlutterView.RenderMode.texture)
        .build();
    
    

    如果要给跳转添加一个转场的透明效果,要启用FlutterFragment的透明属性,可以使用下面的配置,如下所示。

    // Using a new FlutterEngine.
    FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
        .transparencyMode(TransparencyMode.transparent)
        .build();
    
    // Using a cached FlutterEngine.
    FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
        .transparencyMode(TransparencyMode.transparent)
        .build();
    

    FlutterFragment 与Activity

    有时候,一些应用使用Fragment来作为Flutter页面的承载对象时,状态栏、导航栏和屏幕方向仍然使用的是Activity,Fragment只是作为Activity的一部分。在这些应用程序中,用一个Fragment是合理的,如下图所示。

    在其他应用程序中,Fragment仅仅作为UI的一部分,此时一个FlutterFragment可能被用来实现一个抽屉的内部,一个视频播放器,或一个单一的卡片。在这些情况下,FlutterFragment不需要全屏线上,因为在同一个屏幕中还有其他UI片段,如下图所示。

    FlutterFragment提供了一个概念,用来实现FlutterFragment是否能够控制它的宿主Activity。为了防止一个FlutterFragment将它的Activity暴露给Flutter插件,也为了防止Flutter控制Activity的系统UI,FlutterFragment提供了一个shouldAttachEngineToActivity()方法,如下所示。

    // Using a new FlutterEngine.
    FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
        .shouldAttachEngineToActivity(false)
        .build();
    
    // Using a cached FlutterEngine.
    FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
        .shouldAttachEngineToActivity(false)
        .build();
    

    原生iOS集成Flutter

    创建Flutter模块

    为了将 Flutter 集成到原生iOS应用里,第一步要创建一个 Flutter module,创建 Flutter module的命令如下所示。

    cd some/path/
    flutter create --template module my_flutter
    
    

    执行完上面的命令后,会在some/path/my_flutter/ 目录下创建一个Flutter module库。在这个目录中,你可以像在其它 Flutter 项目中一样,执行 flutter 命令,比如 flutter run --debug 或者 flutter build ios。打开 my_flutter 模块,可以发现,目录结构和普通 的Flutter 应用的目录别无二至,如下所示。

    my_flutter/
    ├── .ios/
    │   ├── Runner.xcworkspace
    │   └── Flutter/podhelper.rb
    ├── lib/
    │   └── main.dart
    ├── test/
    └── pubspec.yaml
    

    默认情况下,my_flutter的Android工程和iOS工程是隐藏的,我们可以通过显示隐藏的项目来看到Android工程和iOS工程。

    集成到已有iOS应用

    在原生iOS开发中,有两种方式可以将 Flutter 集成到你的既有应用中。
    1, 使用 CocoaPods 依赖管理和已安装的 Flutter SDK 。(推荐)
    2,把 Flutter engine 、Dart 代码和所有 Flutter plugin 编译成 framework,然后用 Xcode 手动集成到你的应用中,并更新编译设置。

    1, 使用 CocoaPods 和 Flutter SDK 集成

    使用此方法集成Flutter,需要在本地安装了 Flutter SDK。然后,只需要在 Xcode 中编译应用,就可以自动运行脚本来集成Dart 代码和 plugin。这个方法允许你使用 Flutter module 中的最新代码快速迭代开发,而无需在 Xcode 以外执行额外的命令。

    现在假如又一个原生iOS工程,并且 Flutter module 和这个iOS工程是处在相邻目录的,如下所示。

    some/path/
    ├── my_flutter/
    │   └── .ios/
    │       └── Flutter/
    │         └── podhelper.rb
    └── MyApp/
        └── Podfile
    
    

    1,如果你的应用(MyApp)还没有 Podfile,可以根据 CocoaPods 使用指南 来在项目中添加 Podfile。然后,在 Podfile 中添加下面代码:

    flutter_application_path = '../my_flutter'
    load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
    

    2,每个需要集成 Flutter 的 [Podfile target][],执行 install_all_flutter_pods(flutter_application_path),如下所示。

    target 'MyApp' do
      install_all_flutter_pods(flutter_application_path)
    end
    
    

    3,最后,在MyApp原生工程下运行 pod install命令拉取原生工程需要的插件。

    pod install
    

    如果没有任何错误,界面如下图。


    在上面的Podfile文件中, podhelper.rb 脚本会把你的 plugins, Flutter.framework,和 App.framework 集成到你的原生iOS项目中。同时,你应用的 Debug 和 Release 编译配置,将会集成相对应的 Debug 或 Release 的 编译产物。可以增加一个 Profile 编译配置用于在 profile 模式下测试应用。然后,在 Xcode 中打开 MyApp.xcworkspace ,可以使用 【⌘B 】快捷键编译项目,并运行项目即可。

    使用frameworks集成

    除了上面的方法,你也可以创建一个 frameworks,手动修改既有 Xcode 项目,将他们集成进去。但是每当你在 Flutter module 中改变了代码,都必须运行 flutter build ios-framework来编译framework。下面的示例假设你想在 some/path/MyApp/Flutter/ 目录下创建 frameworks。

    flutter build ios-framework --output=some/path/MyApp/Flutter/
    
    

    此时的文件目录如下所示。

    some/path/MyApp/
    └── Flutter/
        ├── Debug/
        │   ├── Flutter.framework
        │   ├── App.framework
        │   ├── FlutterPluginRegistrant.framework (only if you have plugins with iOS platform code)
        │   └── example_plugin.framework (each plugin is a separate framework)
        ├── Profile/
        │   ├── Flutter.framework
        │   ├── App.framework
        │   ├── FlutterPluginRegistrant.framework
        │   └── example_plugin.framework
        └── Release/
            ├── Flutter.framework
            ├── App.framework
            ├── FlutterPluginRegistrant.framework
            └── example_plugin.framework
    
    

    然后,使用 Xcode 打开原生iOS工程,并将生成的 frameworks 集成到既有iOS应用中。例如,你可以在 some/path/MyApp/Flutter/Release/ 目录拖拽 frameworks 到你的应用 target 编译设置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中选择 “Embed & Sign”。

    1, 链接到框架

    当然,你也可以将框架从 Finder 的 some/path/MyApp/Flutter/Release/ 拖到你的目标项目中,然后点击 build settings > Build Phases > Link Binary With Libraries。然后,在 target 的编译设置中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 增加 $(PROJECT_DIR)/Flutter/Release/,如下图所示。


    2,内嵌框架
    生成的动态framework框架必须嵌入你的应用才能在运行时被加载。需要说明的是插件会帮助你生成 静态或动态框架。静态框架应该直接链接而不是嵌入,如果你在应用中嵌入了静态框架,你的应用将不能发布到 App Store 并且会得到一个 Found an unexpected Mach-O header code 的 archive 错误。

    你可以从应用框架组中拖拽框架(除了 FlutterPluginRegistrant 以及其他的静态框架)到你的目标 ‘ build settings > Build Phases > Embed Frameworks,然后从下拉菜单中选择 “Embed & Sign”,如下图所示。

    3,使用 CocoaPods 在 Xcode 和 Flutter 框架中内嵌应用

    除了使用Flutter.framework方式外,你还可以加入一个参数 --cocoapods ,然后将 Flutter 框架作为一个 CocoaPods 的 podspec 文件分发。这将会生成一个 Flutter.podspec 文件而不再生成 Flutter.framework 引擎文件,命令如下。

    flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/
    

    执行命令后,Flutter模块的目录如下图所示。

    some/path/MyApp/
    └── Flutter/
        ├── Debug/
        │   ├── Flutter.podspec
        │   ├── App.framework
        │   ├── FlutterPluginRegistrant.framework
        │   └── example_plugin.framework (each plugin with iOS platform code is a separate framework)
        ├── Profile/
        │   ├── Flutter.podspec
        │   ├── App.framework
        │   ├── FlutterPluginRegistrant.framework
        │   └── example_plugin.framework
        └── Release/
            ├── Flutter.podspec
            ├── App.framework
            ├── FlutterPluginRegistrant.framework
            └── example_plugin.framework
    

    然后,在iOS应用程序使用CocoaPods添加Flutter以来文件即可,如下所示。

    pod 'Flutter', :podspec => 'some/path/MyApp/Flutter/[build mode]/Flutter.podspec'
    
    

    添加一个Flutter页面

    FlutterEngine 和 FlutterViewController

    为了在原生 iOS 应用中展示 Flutter 页面,需要使用到FlutterEngineFlutterViewController。其中,FlutterEngine 充当 Dart VM 和 Flutter 运行时环境; FlutterViewController 依附于 FlutterEngine,给 Flutter 传递 UIKit 的输入事件,并展示被 FlutterEngine 渲染的每一帧画面。

    1,创建一个 FlutterEngine
    创建 FlutterEngine 的时机由您自己决定。作为示例,我们将在应用启动的 app delegate 中创建一个 FlutterEngine,并作为属性暴露给外界。首先,在在 AppDelegate.h文件中添加如下代码。

    @import UIKit;
    @import Flutter;
    
    @interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.
    @property (nonatomic,strong) FlutterEngine *flutterEngine;
    @end
    

    然后,在 AppDelegate.m文件中添加如下代码。

    // Used to connect plugins (only if you have plugins with iOS platform code).
    #import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>
    
    #import "AppDelegate.h"
    
    @implementation AppDelegate
    
    - (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {
      self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];
      // Runs the default Dart entrypoint with a default Flutter route.
      [self.flutterEngine run];
      // Used to connect plugins (only if you have plugins with iOS platform code).
      [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
      return [super application:application didFinishLaunchingWithOptions:launchOptions];
    }
    
    @end
    
    

    需要说明的是,GeneratedPluginRegistrant只有在需要支持的插件才能使用。然后运行项目,结果报了一个framework not found FlutterPluginRegistrant错误。

    ld: warning: directory not found for option '-F/Users/bilibili/Library/Developer/Xcode/DerivedData/iOSFlutterHybird-advitqdrflrsxldrjkqcsvdzxbop/Build/Products/Debug-iphonesimulator/FlutterPluginRegistrant'
    ld: framework not found FlutterPluginRegistrant
    clang: error: linker command failed with exit code 1 (use -v to see invocation)
    
    

    对于这个错误,需要打开项目编译配置,修改Bitcode。默认情况下,Flutter是不支持Bitcode的,Bitcode是一种iOS编译程序的中间代码,在原生iOS工程中集成Flutter需要禁用Bitcode,如下图所示。


    2,使用 FlutterEngine 展示 FlutterViewController
    在下面的例子中,展示了一个普通的 ViewController,当点击页面中的UIButton时就会跳转到 FlutterViewController 的 ,这个 FlutterViewController 使用在 AppDelegate 中创建的 Flutter 引擎 (FlutterEngine)。

    @import Flutter;
    #import "AppDelegate.h"
    #import "ViewController.h"
    
    @implementation ViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // Make a button to call the showFlutter function when pressed.
        UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
        [button addTarget:self
                   action:@selector(showFlutter)
         forControlEvents:UIControlEventTouchUpInside];
        [button setTitle:@"Show Flutter!" forState:UIControlStateNormal];
        button.backgroundColor = UIColor.blueColor;
        button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
        [self.view addSubview:button];
    }
    
    - (void)showFlutter {
        FlutterEngine *flutterEngine =
            ((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;
        FlutterViewController *flutterViewController =
            [[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];
        [self presentViewController:flutterViewController animated:YES completion:nil];
    }
    @end
    
    

    运行上面的代码,如果出现“symbol(s) not found for architecture x86_64”的错误,可以使用下面的步骤进行解决。使用Xcode打开项目,然后依次选择TARGETS->Build Phases,然后找到Compile Sources 并点击“+”, 在搜索框输入APPDelegate 找到他的.m文件。

    3,使用隐式 FlutterEngine 创建 FlutterViewController
    我们可以让 FlutterViewController 隐式的创建 FlutterEngine,而不用提前初始化一个FlutterEngine。不过不建议这样做,因为按需创建FlutterEngine 的话,在 FlutterViewController 被 present 出来之后,第一帧图像渲染完之前,将会有明显的延迟。不过,当 Flutter 页面很少被展示时,可以使用此方式。

    为了不使用已经存在的 FlutterEngine 来展现 FlutterViewController,省略 FlutterEngine 的创建步骤,并且在创建 FlutterViewController 时,去掉 FlutterEngine 的引用。

    // Existing code omitted.
    // 省略已经存在的代码
    - (void)showFlutter {
      FlutterViewController *flutterViewController =
          [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
      [self presentViewController:flutterViewController animated:YES completion:nil];
    }
    @end
    

    使用 FlutterAppDelegate

    FlutterAppDelegate 具备如下功能:

    • 传递应用的回调,例如 openURL 到 Flutter 的插件 —— local_auth。
    • 传递状态栏点击(这只能在 AppDelegate 中检测)到 Flutter 的点击置顶行为。

    我们推荐应用的UIApplicationDelegate 继承 FlutterAppDelegate,但不是必须的,如果你的 App Delegate 不能直接继承 FlutterAppDelegate,那么让你的 App Delegate 实现 FlutterAppLifeCycleProvider 协议,来确保 Flutter plugins 接收到必要的回调。否则,依赖这些事件的 plugins 将会有无法预估的行为。

    @import Flutter;
    @import UIKit;
    @import FlutterPluginRegistrant;
    
    @interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
    @property (strong, nonatomic) UIWindow *window;
    @property (nonatomic,strong) FlutterEngine *flutterEngine;
    @end
    
    

    然后,在具体实现中,将App Delegate委托给 FlutterPluginAppLifeCycleDelegate,如下所示。

    @interface AppDelegate ()
    @property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;
    @end
    
    @implementation AppDelegate
    
    - (instancetype)init {
        if (self = [super init]) {
            _lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
        }
        return self;
    }
    
    - (BOOL)application:(UIApplication*)application
    didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id>*))launchOptions {
        self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
        [self.flutterEngine runWithEntrypoint:nil];
        [GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];
        return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
    }
    
    // Returns the key window's rootViewController, if it's a FlutterViewController.
    // Otherwise, returns nil.
    - (FlutterViewController*)rootFlutterViewController {
        UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
        if ([viewController isKindOfClass:[FlutterViewController class]]) {
            return (FlutterViewController*)viewController;
        }
        return nil;
    }
    
    - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
        [super touchesBegan:touches withEvent:event];
    
        // Pass status bar taps to key window Flutter rootViewController.
        if (self.rootFlutterViewController != nil) {
            [self.rootFlutterViewController handleStatusBarTouches:event];
        }
    }
    
    - (void)application:(UIApplication*)application
    didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {
        [_lifeCycleDelegate application:application
    didRegisterUserNotificationSettings:notificationSettings];
    }
    
    - (void)application:(UIApplication*)application
    didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {
        [_lifeCycleDelegate application:application
    didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
    }
    
    - (void)application:(UIApplication*)application
    didReceiveRemoteNotification:(NSDictionary*)userInfo
    fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
        [_lifeCycleDelegate application:application
           didReceiveRemoteNotification:userInfo
                 fetchCompletionHandler:completionHandler];
    }
    
    - (BOOL)application:(UIApplication*)application
                openURL:(NSURL*)url
                options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
        return [_lifeCycleDelegate application:application openURL:url options:options];
    }
    
    - (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
        return [_lifeCycleDelegate application:application handleOpenURL:url];
    }
    
    - (BOOL)application:(UIApplication*)application
                openURL:(NSURL*)url
      sourceApplication:(NSString*)sourceApplication
             annotation:(id)annotation {
        return [_lifeCycleDelegate application:application
                                       openURL:url
                             sourceApplication:sourceApplication
                                    annotation:annotation];
    }
    
    - (void)application:(UIApplication*)application
    performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
      completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
        [_lifeCycleDelegate application:application
           performActionForShortcutItem:shortcutItem
                      completionHandler:completionHandler];
    }
    
    - (void)application:(UIApplication*)application
    handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
      completionHandler:(nonnull void (^)(void))completionHandler {
        [_lifeCycleDelegate application:application
    handleEventsForBackgroundURLSession:identifier
                      completionHandler:completionHandler];
    }
    
    - (void)application:(UIApplication*)application
    performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
        [_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
    }
    
    - (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
        [_lifeCycleDelegate addDelegate:delegate];
    }
    @end
    
    

    启动选项

    上面例子使用默认配置来启动 Flutter,为了定制化你的 Flutter 运行时,我们可以指定 Dart 入口、库和路由。

    1,指定Dart 入口

    在 FlutterEngine 上调用 run()函数,默认将会调用你的 lib/main.dart 文件里的 main() 函数。不过,我们可以使用入口方法 runWithEntrypoint()来指定一个Dart 入口,并且,使用 main() 以外的 Dart 入口函数,必须使用下面的注解,防止被 tree-shaken 优化掉,而没有进行编译。如下所示。

      @pragma('vm:entry-point')
      void myOtherEntrypoint() { ... };
    
    

    2,指定Dart 库
    同时,Flutter允许开发者在指定 Dart 函数时指定特定文件。例如使用 lib/other_file.dart 文件的 myOtherEntrypoint() 函数取代 lib/main.dart 的 main() 函数,如下所示。

    [flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];
    
    

    3,指定Dart 路由

    当然,当构建Flutter Engine 时,还可以为你的 Flutter 应用设置一个初始路由,如下所示。

    FlutterEngine *flutterEngine =
        [[FlutterEngine alloc] initWithName:@"my flutter engine"];
    [[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute"
                                          arguments:@"/onboarding"];
    [flutterEngine run];
    
    

    </article>

    相关文章

      网友评论

        本文标题:GitHub标星8.3K,字节跳动大牛在原生项目中集成Flutt

        本文链接:https://www.haomeiwen.com/subject/skcbektx.html