Flutter升级到1.12后遇到的问题
前些日子评论区里wangwhatlh同学反馈
遇到了程序包io.flutter.facade不存在问题,起初我运行了一下之前的项目,发现可以正常运行,加上我自己有一段时间没有用过Flutter了,也就没太重视这个问题。说来也是惭愧,最近又陆续有多位小伙伴反馈了这个问题,我才终于意识到这是一个普遍性问题,简单查了一下了解到这个错误是Flutter 1.12版本废弃了io.flutter.facade包导致的,我自己更新了Flutter版本后重新运行项目也遇到了这个问题,所以要对此前遇到这个问题的大家说声抱歉,确实是我没有重视这个问题,之后对于大家提出的问题我一定尽快反馈。
好了,接下来就介绍一下解决方案吧,首先附上官方的一些相关说明文档,大家可以自行阅读一下文档,文档中介绍的还是比较详细的。
Upgrading pre 1.12 Android projects qq
Experimental: Add Flutter View 添加链接描述
Add Flutter to existing app 添加链接描述
下面进入正题,简单介绍一下在Flutter 1.12版本中几个需要修改的地方。
原生页面中引入Flutter
上文在介绍Android原生页面跳转Flutter页面时提到了两种方案:FlutterView和FlutterFragment,我们来分别看一下现在应该如何实现。
首先是通过FlutterView引入Flutter页面,以前我们是通过io.flutter.facade包中Flutter类的createView()方法创建出一个FlutterView,然后添加到Activity的布局中,但是由于io.flutter.facade包的废弃,该方法已经无法使用。官方的文档有说明目前不提供在View级别引入Flutter的便捷API,因此如果可能的话,我们应该避免使用FlutterView,但是通过FlutterView引入Flutter页面也是可行的,代码如下:
// 通过FlutterView引入Flutter编写的页面
FlutterView flutterView = new FlutterView(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout flContainer = findViewById(R.id.fl_container);
flContainer.addView(flutterView, lp);
// 关键代码,将Flutter页面显示到FlutterView中
flutterView.attachToFlutterEngine(flutterEngine);
需要注意,这里的FlutterView位于io.flutter.embedding.android包中,和此前我们所创建的FlutterView(位于io.flutter.view包中)是不一样的。我们通过查看FlutterView的源码可以发现它继承自FrameLayout,因此像一个普通的View那样添加就可以了。接下来的这一步很关键,调用FlutterView的attachToFlutterEngine()方法,这个方法的作用就是将Flutter编写的UI页面显示到FlutterView中,我们注意到这里传入了一个flutterEngine参数,它又是什么呢?flutterEngine的类型为FlutterEngine,字面意思就是Flutter引擎,它负责在Android端执行Dart代码,将Flutter编写的UI显示到FlutterView/FlutterActivity/FlutterFragment中。创建FlutterEngine的代码如下:
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
这样就创建好了一个FlutterEngine对象,默认情况下FlutterEngine加载的路由名称为"/",我们可以通过下面的代码指定初始路由名称:
flutterEngine.getNavigationChannel().setInitialRoute("route1");
至于传参的情况没有变化,直接在路由名称后面拼接参数就可以了。当然,FlutterView也可以直接在xml布局文件中添加,最后同样需要调用attachToFlutterEngine()方法将Flutter编写的UI页面显示到FlutterView中,这里就不展示了。
补充一下,最近我将Flutter版本更新到了1.17,发现上述代码运行后FlutterView无法显示,和官方提供的示例flutter_view进行了对比,才发现缺少了下面的代码:
@Override
protected void onResume() {
super.onResume();
flutterEngine.getLifecycleChannel().appIsResumed();
}
@Override
protected void onPause() {
super.onPause();
flutterEngine.getLifecycleChannel().appIsInactive();
}
@Override
protected void onStop() {
super.onStop();
flutterEngine.getLifecycleChannel().appIsPaused();
}
相信大家都能看出这和生命周期有关,flutterEngine.getLifecycleChannel()获取到的是一个LifecycleChannel对象,类比于MethodChannel,作用大概就是将Flutter和原生端的生命周期相互联系起来。这里分别在onResume()、onPause()和onStop()方法中调用了LifecycleChannel的appIsResumed()、appIsInactive()和appIsPaused()方法,作用就是同步Flutter端与原生端的生命周期。添加上述代码后,FlutterView就可以正常显示了。至于为什么在Flutter 1.17版本(也有可能是更早的版本)中需要添加上述代码,我猜想可能是FlutterVIew的渲染机制有了一些变化,在接收到原生端对应生命周期方法中发送的通知才会显示,具体原理我也不是很清楚,如果有说得不对的地方或是大家有了解这部分内容的欢迎提出。
然后是通过FlutterFragment引入Flutter页面,我们此前是通过Flutter.createFragment()方法创建出FlutterFragment,现在同样无法使用了。官方提供了三种创建FlutterFragment的方式,我们来分别看一下。
方式一、FlutterFragment.createDefault()
// 通过FlutterFragment引入Flutter编写的页面
FlutterFragment flutterFragment = FlutterFragment.createDefault();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fl_container, flutterFragment)
.commit();
通过FlutterFragment.createDefault()创建出FlutterFragment,需要注意这里的FlutterFragment位于io.flutter.embedding.android包中,和我们此前使用的FlutterFragment不是同一个类。创建好之后就没什么可说的了,按照正常的Fragment添加就好。createDefault()方法创建出的Fragment显示的路由名称为"/",如果我们需要指定其他路由名称就不能使用这个方法了。
方式二、FlutterFragment.withNewEngine()
// 通过FlutterFragment引入Flutter编写的页面
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.initialRoute("route1")
.build();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fl_container, flutterFragment)
.commit();
通过FlutterFragment.withNewEngine()获取到NewEngineFragmentBuilder对象,使用建造者模式构造出FlutterFragment对象,可以通过initialRoute()方法指定初始路由名称。同样地,传递参数只需要在路由名称后面进行拼接。
方式三、FlutterFragment.withCachedEngine
// 创建可缓存的FlutterEngine对象
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getNavigationChannel().setInitialRoute("route1");
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);
// 通过FlutterFragment引入Flutter编写的页面
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
.build();
方式二使用的withNewEngine()方法从名称上也能看出每次都是创建一个新的FlutterEngine对象来显示Flutter UI,但是从官方文档中我们可以了解到每个FlutterEngine对象在显示出Flutter UI之前是需要一个warm-up(不知道能不能翻译为预热)期的,这会导致屏幕呈现短暂的空白,解决方式就是预先创建并启动FlutterEngine,完成warm-up过程,然后将这个FlutterEngine缓存起来,之后使用这个FlutterEngine来显示出Flutter UI。上面的代码中执行的FlutterEngineCache.getInstance().put(“my_engine_id”, flutterEngine)就是将FlutterEngine缓存起来,这里传入的"my_engine_id"就相当于缓存名称。之后通过FlutterFragment.withCachedEngine()方法来创建FlutterFragment,参数传入上面的缓存名称。需要注意,withCachedEngine()方法返回的是一个CachedEngineFragmentBuilder对象,同样是使用了建造者模式,但是它是没有initialRoute()方法的,如果我们要指定初始路由,需要在创建FlutterEngine对象时通过setInitialRoute()方法来设置。
除此之外,Flutter 1.12中还提供了一种原生引入Flutter页面方式——使用FlutterActivity,这里的FlutterActivity也是位于io.flutter.embedding.android包下的。下面我简单介绍一下如何通过FlutterActivity引入Flutter编写的UI,大家也可以参考官网的介绍。
首先需要在AndroidManifest.xml文件中注册FlutterActivity,代码如下:
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize" />
这里的theme可以替换为自己项目中定义的主题。注册好FlutterActivity后,第二步就是直接启动这个Activity了,启动FlutterActivity有以下三种方式:
// 方式一、FutterActivity显示的路由名称为"/",不可设置
startActivity(
FlutterActivity.createDefaultIntent(this)
);
// 方式二、FutterActivity显示的路由名称可设置,每次都创建一个新的FlutterEngine对象
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("route1")
.build(this)
);
// 方式三、FutterActivity显示的路由名称可设置,使用缓存好的FlutterEngine对象
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(this)
);
是不是很熟悉,和上面介绍的创建FlutterFragment的三种方式是对应的,这里我就不再介绍了。与通过FlutterView/FlutterFragment引入Flutter UI不同,这种方式不需要我们自己创建一个Activity,FlutterActivity显示的Flutter路由是在创建Intent对象时指定的,优点就是使用起来更简单,缺点就是不够灵活,无法像FlutterView/FlutterFragment那样只是作为原生页面中的一部分展示,因此这种方式更适合整个页面都是由Flutter编写的场景。
在调试阶段,跳转FlutterActivity之后也会黑屏几秒,同样地,打了release包后就没有问题了。