Flutter-集成到现有App项目
-
简介
鉴于Flutter技术刚刚成型,越来越多的人用Flutter来写页面,然后嵌入到原生中去,从而达到页面的代码是通用的。因此写下此篇文章研究整理Flutter集成到原声App中的过程和原理。
-
配置
首先需要创建一个Flutter Module,AndroidStudio可以完成这个工作,或者你已经有了一个Flutter Module,这里贴一下命令行创建方式:
$ cd some/path/ $ flutter create -t module --org com.example my_flutter
然后因为Flutter的AOT编译模式目前仅支持x86_64, armeabi-v7a and arm64-v8a,为了兼容所以在app/build.gradle中添加:
android { //... defaultConfig { ndk { // Filter for architectures supported by Flutter. abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64' } } }
因为目前Flutter的Android engine使用的是Java8,所以在app/build.gradle中添加:
android { //... compileOptions { sourceCompatibility 1.8 targetCompatibility 1.8 } }
接下来开始添加依赖,有下面两种方式:
-
aar方式
进入Flutter Module路径下执行下面命令:
$ cd some/path/my_flutter $ flutter build aar
然后结束后会看到如图信息:
然后按照命令行输出的步骤操作。
这种方式适合有自己远程仓库的情况,当然也可以把repo文件夹放在项目中直接使用相对路径引入,这样团队中的每个人就不会调成一致的本地路径,但是这样又会把原声项目和flutter项目耦合在一起,所以还是适合远程仓库。
-
代码方式
aar方式可以本地引入,我们还可以直接源代码引入,但是它们的配置不太一样。
首先需要把flutter放在一个容易从原生项目文件夹容易找到的地方,比如同级或子目录,最好放在子目录,因为源代码导入意味着flutter项目源码对于原生项目开发者来说也是公开的,所以为了方便统一拷贝和上传,最好放在子目录。
然后在原生项目的settings.gradle中加入:
// Include the host app project. include ':app' // assumed existing content setBinding(new Binding([gradle: this])) // new evaluate(new File( // new // settingsDir.parentFile, // Assuming my_flutter is a sibling to MyApp. settingsDir, // Assuming my_flutter is a child-dir in MyApp. 'flutter_module/.android/include_flutter.groovy' // new ))
在app/build.gradle中加入依赖项:
implementation project(':flutter')
为什么是flutter这个名字呢?看一下include_flutter.groovy中有如下配置:
gradle.include ":flutter" gradle.project(":flutter").projectDir = new File(flutterProjectRoot, ".android/Flutter")
可以看到:flutter就是.android中的Flutter项目,这个项目是根据我们前面的配置从而根据宿主App项目来生成的:
看一下它的iml文件:
<module external.linked.project.id=":flutter" ...> ... ... </module>
可以看到,project.id设置为了:flutter。
-
-
创建FlutterActivity
连接原生和flutter要用sdk的FlutterActivity,我们可以不用继承FlutterActivity,直接使用FlutterActivity来当我们的flutter承载页面。
-
在AndroidManifest.xml中配置
<activity android:name="io.flutter.embedding.android.FlutterActivity" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize" />
和原生一样的配置方式。
-
启动Activity
myButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity( FlutterActivity.createDefaultIntent(currentActivity) ); } });
这种启动方式是以flutter切入点是main()函数并且初始路由是‘/’为前提的,切入点无法通过构造Intent来改变,但是初始路由可以,像下面这样:
myButton.addOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity( FlutterActivity .withNewEngine() .initialRoute("/my_route") .build(currentActivity) ); } });
关于切入点和初始路由是怎么和原生联系起来的下面再说。
你可以在原生程序的任何一个地方通过上面这种传统的Intent启动FlutterActivity,但是上面这种方式每次启动都会构造一个新的Flutter Engine,创建这个东西需要耗费不少时间,至少肉眼上能看出一个显著的延迟,为了达到和打开一个原生页面的速度差不多的目的,我们需要在启动Flutter页面之前就先创建一个Flutter Engine,然后把它缓存起来,打开flutter页面时直接使用就会解决这个问题,官方给我们提供了相关的API来这么做,这里把它放在Application启动时:
public class MyApplication extends Application { public FlutterEngine flutterEngine; @Override public void onCreate() { super.onCreate(); // Instantiate a FlutterEngine. flutterEngine = new FlutterEngine(this); // Start executing Dart code to pre-warm the FlutterEngine. flutterEngine.getDartExecutor().executeDartEntrypoint( DartEntrypoint.createDefault() ); // Cache the FlutterEngine to be used by FlutterActivity. FlutterEngineCache .getInstance() .put("my_engine_id", flutterEngine); } }
这里手动设置了一个切入点,就是engine可以和dart代码进行交互的一个入口,我们看一下DartEntrypoint.createDefault方法:
@NonNull public static DartEntrypoint createDefault() { FlutterLoader flutterLoader = FlutterInjector.instance().flutterLoader(); if (!flutterLoader.initialized()) { throw new AssertionError( "DartEntrypoints can only be created once a FlutterEngine is created."); } return new DartEntrypoint(flutterLoader.findAppBundlePath(), "main"); }
public DartEntrypoint( @NonNull String pathToBundle, @NonNull String dartEntrypointFunctionName) { this.pathToBundle = pathToBundle; dartEntrypointLibrary = null; this.dartEntrypointFunctionName = dartEntrypointFunctionName; }
可以看到,这种默认的方式下设置的dartEntrypointFunctionName就是main。
executeDartEntrypoint方法一旦执行意味着dart中的main方法开始执行了,通常是:
void main() => runApp(MyApp());
此时Engine还没有绑定到FlutterActivity中,所以此时就有一个0尺寸的window存在,直到attatch到FlutterActivity中才显示出来。
FlutterEngineCache就是用来缓存FlutterEngine实例的,这里的key值可以自定义,但是要和后面取的时候保持一致:
myButton.addOnClickListener(new OnClickListener() { @Override public void onClick(View v) { startActivity( FlutterActivity .withCachedEngine("my_engine_id") .build(currentActivity) ); } });
注意,一旦预热Flutter Engine则意味着无论FlutterActivity创建和销毁dart code都会一直在执行,你可以利用它做一些非UI的后台工作,如果要销毁和释放资源需要从FlutterEngineCache中取到它并调用destroy方法。
还要清楚的一点是,Flutter的dubug和release版本有着完全不同的性能表现,如果要测试性能的话用release版本测试。
关于配置初始路由,前面我们知道了使用新Engine的时候如何配置路由,那使用缓存路由的时候就要在执行dart code之前配置,在上面的flutterEngine.getDartExecutor().executeDartEntrypoint代码之前设置:
flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");
注意,初始路由通过Engine指定必须在runApp()方法之前设置,如果在多个Activity或Fragment中使用同一个Engine并且想要不同的初始路由的话就得通过MethodChannel和dart代码交互来完成了。
上面我们知道了如何启动一个普通的Activity,在Android中还有一种是半透明的Activity,比如Dialog,它的创建方式和上面的一样,之不过多了些设置。
首先需要指定一个theme设置android:windowIsTranslucent属性:
<style name="MyTheme" parent="@style/MyParentTheme"> <item name="android:windowIsTranslucent">true</item> </style>
然后在启动时指定bankground:
// 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) );
当然,这种方式只会让PhoneWindow是透明的,其中的内容(即布局)需要自己去控制部分显示,如果选择宽高都match_parent的话也不会是透明的效果。
-
-
创建FlutterFragment
创建方式和传统的Activity中创建是一样的:
public class MyActivity extends FragmentActivity { // Define a tag String to represent the FlutterFragment within this // Activity's FragmentManager. This value can be whatever you'd like. private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment"; // Declare a local variable to reference the FlutterFragment so that you // can forward calls to it later. private FlutterFragment flutterFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Inflate a layout that has a container for your FlutterFragment. // For this example, assume that a FrameLayout exists with an ID of // R.id.fragment_container. setContentView(R.layout.my_activity_layout); // Get a reference to the Activity's FragmentManager to add a new // FlutterFragment, or find an existing one. FragmentManager fragmentManager = getSupportFragmentManager(); // Attempt to find an existing FlutterFragment, // in case this is not the first time that onCreate() was run. flutterFragment = (FlutterFragment) fragmentManager .findFragmentByTag(TAG_FLUTTER_FRAGMENT); // Create and attach a FlutterFragment if one does not exist. if (flutterFragment == null) { flutterFragment = FlutterFragment.createDefault(); fragmentManager .beginTransaction() .add( R.id.fragment_container, flutterFragment, TAG_FLUTTER_FRAGMENT ) .commit(); } } }
当需要响应Activity的相关回调事件时也需要手动调用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); } }
关于构造FlutterFragment,和FlutterActivity类似(包括engine等),只不过换成了FlutterFragment的静态方法,不同的是FlutterFragment的build()会返回一个FlutterFragment实例,此外FlutterFragment的构建过程还多了一些API。
FlutterFragment可以在构造的时候指定entryPoint:
FlutterFragment flutterFragment = FlutterFragment.withNewEngine() .dartEntrypoint("mySpecialEntrypoint") .build();
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();
RenderMode有三种渲染模式:surface、texture、image。
surface模式有着最佳的性能表现,是默认的模式,但是它要求flutter UI必须在其他Android View的上方;texture模式性能不如surface,但是它是为了可以和Android View自由嵌套而生的;image模式,主要作用是此模式下,FlutterView可以和Android原生View完全交互。
FlutterFragment也支持透明渲染模式,来处理一些将底部的:
// Using a new FlutterEngine. FlutterFragment flutterFragment = FlutterFragment.withNewEngine() .transparencyMode(FlutterView.TransparencyMode.transparent) .build(); // Using a cached FlutterEngine. FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id") .transparencyMode(FlutterView.TransparencyMode.transparent) .build();
另外,大部分场景下Fragment都是作为window的一部分来展示,还有一种场景是Frgament是Activity的全部,所以Fragment的所有和window的行为都要通过Activity去执行,这种情况下,FlutterFragment有一个API可以设置是否允许FlutterFragment的engine拥有所处Activity的引用:
// 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();