版本号0.1.54
看源码之前,我先去看下官方文档,对于其源码的设计说明,文中所说的原生都是指android
看完官方文档的说明,我有以下几个疑问
第一个:容器是怎么设计的?
第二个:native和flutter的channel的通道是如何设计的?
第三个:Flutter是适配层到底再做些什么?
中控中心FlutterBoost
单独拎出来讲讲,这个类比较简单,就是集合各个模块并让其初始化,同时也是该插件入口处,不管原生和flutter都一样,看源码也是从这里开始看起,但原生和flutter的初始化流程稍微有少许区别,主要还是因为原生是作为容器,flutter的容器是依赖于原生容器。
原生init
入口:/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MyApplication.java
FlutterBoost.init从这里开始进入
FlutterBoost.init(new Platform() {
@Override
public Application getApplication() {
return MyApplication.this;
}
@Override
public boolean isDebug() {
return true;
}
@Override
public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
PageRouter.openPageByUrl(context, url, urlParams, requestCode);
}
@Override
public IFlutterEngineProvider engineProvider() {
//注意这里 覆写了createEngine
return new BoostEngineProvider() {
@Override
public BoostFlutterEngine createEngine(Context context) {
return new BoostFlutterEngine(context, new DartExecutor.DartEntrypoint(
context.getResources().getAssets(),
FlutterMain.findAppBundlePath(context),
"main"), "/");
}
};
}
@Override
public int whenEngineStart() {
return ANY_ACTIVITY_CREATED;
}
});
BoostChannel.addActionAfterRegistered(new BoostChannel.ActionAfterRegistered() {
@Override
public void onChannelRegistered(BoostChannel channel) {
//platform view register should use FlutterPluginRegistry instread of BoostPluginRegistry
TextPlatformViewPlugin.register(FlutterBoost.singleton().engineProvider().tryGetEngine().getPluginRegistry());
}
});
}
上面大部分方法,做过android也知道是干嘛的,这里重点讲讲IFlutterEngineProvider这个接口,这里有3个方法,如下
/**
* a flutter engine provider
*/
public interface IFlutterEngineProvider {
/**
* create flutter engine, we just hold a single instance now
* @param context
* @return
*/
BoostFlutterEngine createEngine(Context context);
/**
* provide a flutter engine
* @param context
* @return
*/
BoostFlutterEngine provideEngine(Context context);
/**
* may return null
* @return
*/
BoostFlutterEngine tryGetEngine();
}
抽象成接口,根据项目的实际情况,开发者可以自己实现flutter引擎,或采用官方源码里自己的实现类即BoostEngineProvider,但想不明白 createEngine 和provideEngine 到底有何区别,到底为何弄成两个方法,不就是个提供个flutter引擎实例吗?
看了下createEngine的实现,主要加载实例BoostFlutterEngine,这个实例看名字也清楚是进行flutter引擎的初始化,设置了dart默认入口点即main,设置了路由起点及插件的声明注册一类
然后去看provideEngine方法的实现,代码较少,如下
@Override
public BoostFlutterEngine provideEngine(Context context) {
Utils.assertCallOnMainThread();
if (mEngine == null) {
FlutterShellArgs flutterShellArgs = new FlutterShellArgs(new String[0]);
FlutterMain.ensureInitializationComplete(
context.getApplicationContext(), flutterShellArgs.toArray());
mEngine = createEngine(context.getApplicationContext());
final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
if(stateListener != null) {
stateListener.onEngineCreated(mEngine);
}
}
return mEngine;
}
初始化flutter参数及增加一个回调,没什么特别之处,然后去翻了下flutter.jar的FlutterActivity源码,它的flutter引擎初始化最后是追踪到FlutterFragment,关键代码如下
public void onAttach(Context context) {
super.onAttach(context);
//这里初始化flutter参数
this.initializeFlutter(this.getContextCompat());
if (this.flutterEngine == null) {
//这里是初始化flutter引擎
this.setupFlutterEngine();
}
this.platformPlugin = new PlatformPlugin(this.getActivity(), this.flutterEngine.getPlatformChannel());
}
这里是连在一起的,flutter源码没有翻来覆去全看一遍,闲鱼进行这样的接口设计应该是有一定的原因
这里再单独讲下插件的注册,我们知道native是作为插件库需要原生项目依赖,在初始化中,注意一下插件的注册,是用反射实现的,如下
路径:flutter_boost/android/src/main/java/com/idlefish/flutterboost/BoostFlutterView.java 中的init方法,代码如下
private void init() {
...
mFlutterEngine.startRun((Activity)getContext());
...
}
跟随startRun方法深入,就会找到FlutterBoost.singleton().platform().registerPlugins(mBoostPluginRegistry),跟着下去,会发现使用反射方式来实现插件注册 如下代码
@Override
public void registerPlugins(PluginRegistry registry) {
try {
Class clz = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant");
Method method = clz.getDeclaredMethod("registerWith",PluginRegistry.class);
method.invoke(null,registry);
}catch (Throwable t){
throw new RuntimeException(t);
}
}
毕竟引擎初始化框架重新编写了,所以在插件的注册上也改变了,init的原生部分就讲解到此
flutter
入口:/flutterProject/flutter_boost/example/lib/main.dart
flutter的源码查看前,大家务必先去看看flutter的初始化流程,Navigator源码解析及Route源码解析,因为不晓得相关初始化流程及Navigator的设计原理,里面的关键调用 大家都可能看不明白,我这边可能也是直接就过了,这里给个链接大家可以去看看
@override
void initState() {
super.initState();
print('_MyAppState initState');
///路由注册,原生通过MethodChannel通道来启动对应的flutter页面
FlutterBoost.singleton.registerPageBuilders({
'first': (pageName, params, _) => FirstRouteWidget(),
'second': (pageName, params, _) => SecondRouteWidget(),
'tab': (pageName, params, _) => TabRouteWidget(),
'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params),
///可以在native层通过 getContainerParams 来传递参数
'flutterPage': (pageName, params, _) {
print("flutterPage params:$params");
return FlutterRouteWidget();
},
});
}
@override
Widget build(BuildContext context) {
print('_MyAppState build');
return MaterialApp(
title: 'Flutter Boost example',
builder: FlutterBoost.init(postPush: _onRoutePushed),
home: Container());
}
///flutter 路由push 监听,每启动一个新的flutter页面 就回调该方法
void _onRoutePushed(
String pageName, String uniqueId, Map params, Route route, Future _) {
print('pageName'+pageName+"\n");
}
先跟随FlutterBoost.singleton进去看看,其构造函数如下
FlutterBoost(){
Logger.log('FlutterBoost 构造函数');
ContainerCoordinator(_boostChannel);
}
跟随着ContainerCoordinator去看看,ContainerCoordinator和BoostChannel如何一起来维护通信通道,ContainerCoordinator构造函数如下
ContainerCoordinator(BoostChannel channel) {
assert(_instance == null);
_instance = this;
channel.addEventListener("lifecycle",
(String name, Map arguments) => _onChannelEvent(arguments));
channel.addMethodHandler((MethodCall call) => _onMethodCall(call));
}
增加native生命周期监听,增加方法监听,再去看看_onChannelEvent和_onMethodCall方法就应该清楚ContainerCoordinator其实就是翻译员,将与原生通信的协议进行解释翻译,根据传过来的事件名,方法名 逐一进行需要的框架处理,BoostChannel其实是声明通道,将接收和发送功能进行封装,接收natvie传来的信息,将从flutter的信息发送到native,当然也做了一部分的框架业务处理,将event和method事件进行区分
分发,个人觉得将该功能直接丢至ContainerCoordinator处理可能更好点,应该是出于为了区分event和method特意在BoostChannel进行处理
接下来看看ContainerCoordinator对于native传过来的通信数据处理,代码如下,就分为之前说的event和method两类,代码注释也写了
/// 对native 整个应用的 生命周期 进行抽象出的几个行为事件,让flutter做相应的处理
/// android端 基本上除了有回退事件的处理,剩余的生命周期 仅仅是做了监听没做任何处理
/// 分别是回退处理 android才有
/// foreground 本应用是处于前台
/// background 本应用是处于后台
/// scheduleFrame 触发一帧的绘制,但ios和android 都没找到发送该事件的代码,老版本遗留代码?
Future<dynamic> _onChannelEvent(dynamic event) {
...
}
/// 对native view生命周期(在android 就是activity)进行抽象出的 几个行为事件,
/// 让flutter做相应的框架处理
Future<dynamic> _onMethodCall(MethodCall call) {
...
}
这里就不讲Method的处理逻辑,后面会结合容器部分重点讲
接下来再回到main.dart文件,跟随FlutterBoost.init方法进去看一下,就是初始化BoostContainerManager,不再深入,后面会结合起来一起讲BoostContainerManager
channle
这模块代码比较少,先从这模块开始讲起
我们知道原生和flutter之间的通信就是通过MethodChannel这个类实现的(原生和flutter的类名一样),前面有讲flutter的boost_channel.dart的作用,native的BoostChannel其实也一样,将接收和发送功能进行封装,接收flutter传来的信息,将从native的信息发送到flutter
原生部分
前面原生初始化 讲到插件的注册是通过反射实现的,GeneratedPluginRegistrant.java当中的registerWith方法我们接下去看一下,注册的时候做了哪些事,路径lutter_boost/android/src/main/java/com/idlefish/flutterboost/BoostChannel.java
public static void registerWith(PluginRegistry.Registrar registrar) {
sInstance = new BoostChannel(registrar);
//通道注册后,处理flutter的method 调用处理
for(ActionAfterRegistered a : sActions) {
a.onChannelRegistered(sInstance);
}
//状态监听 回调
if(FlutterBoost.sInstance != null) {
final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
if (stateListener != null) {
stateListener.onChannelRegistered(registrar, sInstance);
}
}
sActions.clear();
}
看到了吧,BoostChannel的实例化是在插件注册的时候进行的,继续深入,如下代码
private BoostChannel(PluginRegistry.Registrar registrar){
mMethodChannel = new MethodChannel(registrar.messenger(), "flutter_boost");
mMethodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if (methodCall.method.equals("__event__")) {
String name = methodCall.argument("name");
Map args = methodCall.argument("arguments");
Object[] listeners = null;
synchronized (mEventListeners) {
Set<EventListener> set = mEventListeners.get(name);
if (set != null) {
listeners = set.toArray();
}
}
if(listeners != null) {
for(Object o:listeners) {
((EventListener)o).onEvent(name,args);
}
}
}else{
Object[] handlers;
synchronized (mMethodCallHandlers) {
handlers = mMethodCallHandlers.toArray();
}
for(Object o:handlers) {
((MethodChannel.MethodCallHandler)o).onMethodCall(methodCall,result);
}
}
}
});
}
对于通道上的数据分为两类event和method,都是和flutter一一对应的,前面flutter初始化中,也讲过,在原生这边event 没有框架上的业务处理,但提供了回调,根据自己的业务是否需要增加监听
method的处理,去查看BoostMethodHandler,FlutterBoost.java作为内部类存在,如下
class BoostMethodHandler implements MethodChannel.MethodCallHandler {
@Override
public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {
switch (methodCall.method) {
case "pageOnStart":
...
break;
case "openPage":
...
break;
case "closePage":
...
break;
case "onShownContainerChanged":
...
break;
default:
{
result.notImplemented();
}
}
}
}
看这些switch的case处理,大概也猜出来是干嘛的了,是flutter通知native的页面行为事件,例openPage,closePage等等 ,在初始化中,讲了挺多flutter的chanell,所以这里就不在讲了,但是讲讲两边的设计
两边都有个channel类,主要都是用来接收和发送消息的
flutter专门有一个类ContainerCoordinator.dart,中文翻译过来就是集装箱协调员,用于通信事件的统一处理,就是将从原生接收到的信息进行处理,但是在原生那边并没有类似的类,而是将这个工作放在FlutterBoost.java这个内部类中,个人觉得为了保持统一可以专门抽象出个类,将该功能放置该类中,放在FlutterBoost.java不能保持高度统一且不雅观吧
讲到这里,其实通道的设计大家应该理解得差不多了(解决开头提出的问题)
native和flutter的channel的通道是如何设计的?
容器
原生部分
闲鱼的栈管理方案,是将栈的管理都放置原生,所以在原生必须暴露栈的管理,让项目接入方能在原有栈的解决方案上 融合进闲鱼的栈管理方案,所以页面的打开就是入口处,从该入口处去查看容器的设计,先从demo中的PageRouter.java看起,如下代码
public class PageRouter {
public static final String NATIVE_PAGE_URL = "sample://nativePage";
public static final String FLUTTER_PAGE_URL = "sample://flutterPage";
public static final String FLUTTER_FRAGMENT_PAGE_URL = "sample://flutterFragmentPage";
public static boolean openPageByUrl(Context context, String url,Map params) {
return openPageByUrl(context, url,params, 0);
}
public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {
try {
if (url.startsWith(FLUTTER_PAGE_URL)) {
context.startActivity(new Intent(context, FlutterPageActivity.class));
return true;
} else if (url.startsWith(FLUTTER_FRAGMENT_PAGE_URL)) {
context.startActivity(new Intent(context, FlutterFragmentPageActivity.class));
return true;
} else if (url.startsWith(NATIVE_PAGE_URL)) {
context.startActivity(new Intent(context, NativePageActivity.class));
return true;
} else {
return false;
}
} catch (Throwable t) {
return false;
}
}
}
如果大家用过阿里的Aroute路由框架,就会觉得很亲切,将每个View配置一个路由,还是前端的思想借鉴过来,一个统一的界面打开处,根据路由路径,判断是原生view还是FlutterView,分别打开不同的Activity
接入方,在这里可以根据自身的原生栈管理再进行抽象封装就ok了
接下来看看哪里调用了openPageByUrl(注意是下面那个)方法,发现正是我们一开始框架初始化的时候在调用,如下,文件路径flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MyApplication.java
@Override
public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
PageRouter.openPageByUrl(context, url, urlParams, requestCode);
}
再查看是哪里调用了该方法,一直追踪到FlutterBoost.java,关键代码如下
case "openPage":
{
try {
Map<String,Object> params = methodCall.argument("urlParams");
Map<String,Object> exts = methodCall.argument("exts");
String url = methodCall.argument("url");
mManager.openContainer(url, params, exts, new FlutterViewContainerManager.OnResult() {
@Override
public void onResult(Map<String, Object> rlt) {
if (result != null) {
result.success(rlt);
}
}
});
}catch (Throwable t){
result.error("open page error",t.getMessage(),t);
}
}
Flutter 通过channel通道 通知原生 要打开一个新的页面,然后原生将自身的生命周期通过通道告知flutter,flutter再进行相应的页面处理,虽然短短一句话,但其中的逻辑及代码量还是很多的...
前面已经讲了通道部分,这里再贴点关键代码,原生将自身的生命周期通过通道告知flutter,关键代码如下
private class MethodChannelProxy {
private int mState = STATE_UNKNOW;
private void create() {
...
}
private void appear() {
...
}
private void disappear() {
...
}
}
private void destroy() {
..
}
public void invokeChannel(String method, String url, Map params, String uniqueId) {
...
}
public void invokeChannelUnsafe(String method, String url, Map params, String uniqueId) {
..
}
}
public static String genUniqueId(Object obj) {
return System.currentTimeMillis() + "-" + obj.hashCode();
}
}
ok,现在已经找到了MethodChannelProxy类,那我们就继续回找(注意我这里讲解都是从冰山一角再慢慢往上查,最终再将冰山一起探索完毕)MethodChannelProxy是作为ContainerRecord.java的内部类存在。接下来我们来看ContainerRecord类,其实现了IContainerRecord接口,再继续深究找到IOperateSyncer接口,代码如下
public interface IOperateSyncer {
void onCreate();
void onAppear();
void onDisappear();
void onDestroy();
void onBackPressed();
void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults);
void onNewIntent(Intent intent);
void onActivityResult(int requestCode, int resultCode, Intent data);
void onContainerResult(int requestCode, int resultCode, Map<String,Object> result);
void onUserLeaveHint();
void onTrimMemory(int level);
void onLowMemory();
}
该接口是通过对原生的生命周期再结合flutter的生命周期特色及android自身的特性(有回退物理键)抽象出来的,继续回到接下来我们来看ContainerRecord类,发现MethodChannelProxy类其实就是做个代理功能,看名字也清楚,在接口方法被调用的时候,通知flutter
接下来看看IContainerRecord的方法被调用处,追踪到BoostFlutterActivity.java和BoostFlutterFragment.java,这里我们只看Activity,Fragment基本差不多。我们看到BoostFlutterActivity在走onCreate生命周期方法时,创建了mSyncer,关键代码如下
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureWindowForTransparency();
mSyncer = FlutterBoost.singleton().containerManager().generateSyncer(this);
mFlutterEngine = createFlutterEngine();
mFlutterView = createFlutterView(mFlutterEngine);
setContentView(mFlutterView);
mSyncer.onCreate();
configureStatusBarForFullscreenFlutterExperience();
}
FlutterBoost.singleton().containerManager().generateSyncer() 继续深入,追踪到FlutterViewContainerManager类,关键代码如下
@Override
public IOperateSyncer generateSyncer(IFlutterViewContainer container) {
Utils.assertCallOnMainThread();
//创建容器记录实例
ContainerRecord record = new ContainerRecord(this, container);
if (mRecordMap.put(container, record) != null) {
Debuger.exception("container:" + container.getContainerUrl() + " already exists!");
}
mRefs.add(new ContainerRef(record.uniqueId(),container));
//讲接口引用返回
return record;
}
ContainerRecord实例在这里创建,同时将接口引用返给Activity,这样就和原生View的生命周期关联起来了
注意到这里我们已经追踪到FlutterViewContainerManager.java类,已经可以从上帝视角去看了。
刚刚从容器打开出入一直追踪到FlutterViewContainerManager.java类,该类看名字就清楚就是容器的管理者,容器创建、打开、关闭、销毁、弹出、移除等等工作都是在这儿,这里最关键的generateSyncer方法刚刚追踪的时候已经讲过。这里再重点讲讲该类的setContainerResult方法,如下
void setContainerResult(IContainerRecord record,int requestCode, int resultCode, Map<String,Object> result) {
IFlutterViewContainer target = findContainerById(record.uniqueId());
if(target == null) {
Debuger.exception("setContainerResult error, url="+record.getContainer().getContainerUrl());
}
if (result == null) {
result = new HashMap<>();
}
result.put("_requestCode__",requestCode);
result.put("_resultCode__",resultCode);
final OnResult onResult = mOnResults.remove(record.uniqueId());
if(onResult != null) {
onResult.onResult(result);
}
}
单独拎出来讲,主要是本人好奇 目标页 向 起始面 如何传输数据的,
在纯ntive就是靠着onActivityResult回调拿到目标页传回的数据,该方法就是处理目标页传回来后的处理
在混合栈中 就分为3种情况
1.native-flutter
2.flutter-native
3.flutter-flutter
native和ntive就不用说了,都用不到该框架
第一种情况:native-flutter
demo自身当中并没有相关的演示代码,于是我按照原生是如何接受传回来的数据去进行更改,改了两处如下,类路径flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/MainActivity.java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
data.getSerializableExtra(IFlutterViewContainer.RESULT_KEY);
Debuger.log("MainActivityResult"+data.getSerializableExtra(IFlutterViewContainer.RESULT_KEY).toString());
}
还有一处,类路径flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/PageRouter.java
如下
public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {
try {
if (url.startsWith(FLUTTER_PAGE_URL)) {
//接受目标页的回传必须通过startActivityForResult进行打开
((Activity)context).startActivityForResult(new Intent(context, FlutterPageActivity.class),requestCode);
return true;
} else if (url.startsWith(FLUTTER_FRAGMENT_PAGE_URL)) {
context.startActivity(new Intent(context, FlutterFragmentPageActivity.class));
return true;
} else if (url.startsWith(NATIVE_PAGE_URL)) {
context.startActivity(new Intent(context, NativePageActivity.class));
return true;
} else {
// context.startActivity(new Intent(context, FlutterTwoPageActivity.class));
return false;
}
} catch (Throwable t) {
return false;
}
}
还有记得修改调起的Flutter页面是'second',因为demo中只有它才有传回数据,实现原理这里我简单描述,不详细讲了,就是flutter在关闭页面的时候,传回数据,如下
class SecondRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: Center(
child: RaisedButton(
onPressed: () {
// Navigate back to first route when tapped.
BoostContainerSettings settings =
BoostContainer.of(context).settings;
FlutterBoost.singleton.close(settings.uniqueId,
result: {"result": "data from second"});
},
child: Text('Go back with result!'),
),
),
);
}
}
跟踪关闭代码逻辑,通过之前建立的通信通道,传过去相关方法,即closePage,原生接收到之后的逻辑代码处理如下(类文件路径flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java):
case "closePage":
{
try {
String uniqueId = methodCall.argument("uniqueId");
Map<String,Object> resultData = methodCall.argument("result");
Map<String,Object> exts = methodCall.argument("exts");
mManager.closeContainer(uniqueId, resultData,exts);
result.success(true);
}catch (Throwable t){
result.error("close page error",t.getMessage(),t);
}
}
追踪closeContainer,一直追踪到BoostFlutterActivity.java的finishContainer方法,如下
@Override
public void finishContainer(Map<String,Object> result) {
if(result != null) {
FlutterBoost.setBoostResult(this,new HashMap<>(result));
finish();
}else{
finish();
}
}
再跟踪下去,逻辑很明朗了就不详细讲了
第二种情况:flutter-native
闲鱼的混合栈方案里,每个flutter都有自己的独立原生宿主View,所以回调也得依赖原生
原生我们知道生命周期里就有回调方法,即onActivityResult方法,但是Flutter并没有该方法,闲鱼的混合框架里也并没有专门把这个生命周期给抽出来,本人更倾向于把这个给抽出来,这样框架也比较清晰。不过现在很多原生业务都已经很少用这种方式进行页面传值,因为业务复杂起来,用这种方式反而更麻烦,所以原生就出现了很多eventBus类似的通信框架,所以设计混合栈框架的时候,就直接忽略,而直接用自带的flutter api来实现该功能,怎么实现的?继续看
先看下invokeMethod这个方法,原生和flutter都会有个回调函数,flutter页面拿到目标页的数据传回就是采用该方法,接下来咱们去看flutter页面打开native页面开始看起,类路径flutterProject/flutter_boost/example/lib/simple_page_widgets.dart,关键代码如下:
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text(
'open native page',
style: TextStyle(fontSize: 22.0, color: Colors.black),
)),
///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。
///例如:sample://nativePage?aaa=bbb
onTap: () =>
FlutterBoost.singleton.open("sample://nativePage", urlParams: {
"query": {"aaa": "bbb"}
}).then((Map value) {
print(
"call me when page is finished. did recieve second route result $value");
}),
)
FlutterBoost.singleton.open 跟踪下去,发现最终调用的就是invokeMethod,
如下
Future<Map<dynamic,dynamic>> open(String url,{Map<dynamic,dynamic> urlParams,Map<dynamic,dynamic> exts}){
Map<dynamic, dynamic> properties = new Map<dynamic, dynamic>();
properties["url"] = url;
properties["urlParams"] = urlParams;
properties["exts"] = exts;
return channel.invokeMethod<Map<dynamic,dynamic>>(
'openPage', properties);
}
然后返回的Future,异步的回调函数,拿到原生页面的回传数据。这里的逻辑很简单,重点是原生那边怎么保存该回调,然后在关闭容器的时候进行调用 回调函数以此将数据传给Flutter
接下来看原生对于openPage的处理,之前在讲通道的时候提过,类路径
flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java,代码如下
case "openPage":
{
try {
Map<String,Object> params = methodCall.argument("urlParams");
Map<String,Object> exts = methodCall.argument("exts");
String url = methodCall.argument("url");
mManager.openContainer(url, params, exts, new FlutterViewContainerManager.OnResult() {
@Override
public void onResult(Map<String, Object> rlt) {
if (result != null) {
result.success(rlt);
}
}
});
}catch (Throwable t){
result.error("open page error",t.getMessage(),t);
}
}
break;
重点看 mManager.openContainer方法,传入了一个回调函数,最后调用
result.success(rlt);,数据就传回flutter页面,接下来我们跟踪去看看如何去保存该回调并 最终调用回调
继续跟踪openContainer方法,代码如下:
void openContainer(String url, Map<String, Object> urlParams, Map<String, Object> exts,OnResult onResult) {
Context context = FlutterBoost.singleton().currentActivity();
if(context == null) {
context = FlutterBoost.singleton().platform().getApplication();
}
if(urlParams == null) {
urlParams = new HashMap<>();
}
int requestCode = 0;
final Object v = urlParams.remove("requestCode");
if(v != null) {
requestCode = Integer.valueOf(String.valueOf(v));
}
final String uniqueId = ContainerRecord.genUniqueId(url);
urlParams.put(IContainerRecord.UNIQ_KEY,uniqueId);
if(onResult != null) {
mOnResults.put(uniqueId,onResult);
}
FlutterBoost.singleton().platform().openContainer(context,url,urlParams,requestCode,exts);
}
注意 这里有个mOnResults的Map类型参数,就是保存回调函数,通过每个Flutter页面容器的uniqueId做key(只有Flutter页面会建立容器),但前提是该起始容器打开的时候必须传入Key,不然就无法回调,因为找不到该回调了。这就会出现一个问题,就是我们第一个打开的Flutter页面并不是通过onePage打开的,而是直接通过Context.startActivity方法打开,那么就不会保存该回调,也就无法将目标页的数据传回起始页了,已经反馈给闲鱼官方了,本人想过几种方式,为了这个简单的功能,就破坏整体框架得不偿失,等闲鱼官方更优雅的解决方式吧
继续这个mOnResults这个参数,验证我们的猜想,看看哪里在使用,刚刚只是写入回调函数,就找到setContainerResult这个方法,就回到刚刚说要重点讲的方法那了,关键代码如下:
void setContainerResult(IContainerRecord record,int requestCode, int resultCode, Map<String,Object> result) {
....
final OnResult onResult = mOnResults.remove(record.uniqueId());
Debuger.log("setContainerResult uniqueId "+record.uniqueId());
if(onResult != null) {
Debuger.log("onResult has result");
onResult.onResult(result);
}
}
看看哪里有调用这个方法,发现有两处,一处就是原生的生命周期onDestroy的时候,代码如下:
@Override
public void onDestroy() {
...
mManager.setContainerResult(this,-1,-1,null);
...
}
这个是当前页面销毁的时候,但并没有数据传回,明显不是
ok,继续看另外追踪后的一处关键代码,
代码如下:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
...
mSyncer.onContainerResult(requestCode,resultCode,result);
}
ok,Flutter起始页拿到native传回的数据
第三种情况:flutter-flutter
这里不细讲了,因为就是第一种和第二种的逻辑区分,无非目标页不太一样,传值就是第一种情况的逻辑,拿值就是第二种情况的逻辑
原生关于容器部分,再讲下IFlutterViewContainer.java和IContainerRecord.java 这两个类,因为容器部分主要就是围绕 这两个抽象出来的接口进行一系列的框架实现,这里面做了相当多的抽象,IContainerRecord.java比较好理解,对原生View 生命周期的部分方法抽象,例:onCreate方法,通知flutter页面可以做一些初始化工作(这里面就涉及到flutter容器部分了),还有引擎部分的部分方法抽象等
IFlutterViewContainer.java这个类主要是用于业务代码使用的,你可以看它的实现类都是在demo当中,然后抽象出的方法都是传参,传路由路径,容器关闭时的参数回传等等
好了原生容器的讲解就到此,应该还是遗漏了不少细节的地方,本人觉得好理解就直接过去了
Flutter部分
讲这一部分之前,我们得先了解个flutter的一个widget 叫做Overlay!
了解这玩意,就能弄清楚混合栈是如何做flutter页面的栈,这个组件最大的特点就是提供了动态的在Flutter的渲染树上插入布局的特性。那岂不是很适合Toast这样的场景? 是的去Google下,发现的全是用Overlay来做Toast功能
基于Overlay的特性,就可以用全屏非透明的Overlay,每增加一个flutter页面就增加一个包含自定义的Widget的OverlayEntry,然后覆盖在上一个OverlayEntry上,用户反正看到的只是覆盖在最顶层的OverlayEntry,如果还不能理解可以看看这篇文章
ok,背景交代完毕,现在要去看闲鱼如何设计的这个容器及页面栈,我们就从打开第一个flutter页面作为入口开始看起。类路径:flutterProject/flutter_boost/example/android/app/src/main/java/com/taobao/idlefish/flutterboostexample/PageRouter.java,第一个打开的Flutter页面是FlutterPageActivity.java,前面在讲通道设计的时候,讲到过原生生命周期和Flutter生命周期的绑定,提到过一个抽象出来的接口IOperateSyncer.java,先从onCreate方法开始看起,经过生命周期绑定调用,生成原生容器,提取定义好的通道参数,然后经过通道通信,最后追踪到flutter的didInitPageContainer的方法处理
(类路径flutterProject/flutter_boost/lib/container/container_coordinator.dart)
继续跟踪,中间会经过生命周期的监听调用,最终调用_createContainerSettings方法
(类路径flutterProject/flutter_boost/lib/container/container_coordinator.dart),代码如下
BoostContainerSettings _createContainerSettings(
String name, Map params, String pageId) {
Widget page;
final BoostContainerSettings routeSettings = BoostContainerSettings(
uniqueId: pageId,
name: name,
params: params,
builder: (BuildContext ctx) {
//Try to build a page using keyed builder.
if (_pageBuilders[name] != null) {
page = _pageBuilders[name](name, params, pageId);
}
//Build a page using default builder.
if (page == null && _defaultPageBuilder != null) {
page = _defaultPageBuilder(name, params, pageId);
}
assert(page != null);
Logger.log('build widget:$page for page:$name($pageId)');
return page;
});
return routeSettings;
}
根据方法,再过一遍代码,就是flutter页面容器的参数配置,同时找到一开始注册好的page页面
接下来跟踪原生的appear方法,同样的一阵信号传输...,最终进入flutter的ContainerCoordinator.dart类中的didShowPageContainer方法,继续跟踪,追踪到flutter_boost/lib/container/container_coordinator.dart的showContainer方法
注意的是 flutter容器初始化的过程中做了很多兼容工作,兼容ios兼容android,毕竟两个平台的生命周期是有所差别,但最终要抽象成一样的生命周期,所以要做不少的兼容工作,例如连续2次(didInitPageContainer和didShowPageContainer)进行初始化flutter容器参数
继续看showContainer方法,代码如下
void showContainer(BoostContainerSettings settings) {
if (settings.uniqueId == _onstage.settings.uniqueId) {
_onShownContainerChanged(null, settings.uniqueId);
return;
}
final int index = _offstage.indexWhere((BoostContainer container) =>
container.settings.uniqueId == settings.uniqueId);
//页面的重新显示
if (index > -1) {
_offstage.add(_onstage);
_onstage = _offstage.removeAt(index);
setState(() {});
for (BoostContainerObserver observer in FlutterBoost
.singleton.observersHolder
.observersOf<BoostContainerObserver>()) {
observer(ContainerOperation.Onstage, _onstage.settings);
}
Logger.log('ContainerObserver#2 didOnstage');
} else {
//push flutter栈
pushContainer(settings);
}
}
这里的逻辑很简单,重点看下pushContainer方法,代码如下
void pushContainer(BoostContainerSettings settings) {
assert(settings.uniqueId != _onstage.settings.uniqueId);
assert(_offstage.every((BoostContainer container) =>
container.settings.uniqueId != settings.uniqueId));
//将当前页面的add
_offstage.add(_onstage);
//需要push的页面容器创建
_onstage = BoostContainer.obtain(widget.initNavigator, settings);
setState(() {});
//观察者回调
for (BoostContainerObserver observer in FlutterBoost
.singleton.observersHolder
.observersOf<BoostContainerObserver>()) {
observer(ContainerOperation.Push, _onstage.settings);
}
Logger.log('ContainerObserver#2 didPush');
}
flutter的容器的创建,调用setState方法,跟随进去,发现一个东西,一般flutter页面开发都用不着,就是SchedulerBinding,这里有个文章介绍,这里我简单讲解下,我们可以想想flutter的启动流程中,肯定是有个调度节点,例如:Widget什么时候处理build,什么时候处理动画计算等,就是调度。我们如果要写框架,肯定是要对flutter的调度 得清楚,这样才能写出闲鱼这样的混合栈方案,代码如下
@override
void setState(VoidCallback fn) {
Logger.log('BoostContainerManager setState');
if (SchedulerBinding.instance.schedulerPhase ==
SchedulerPhase.persistentCallbacks) {
//主要在下一帧之前,做一些清理工作或者准备工作
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
Logger.log('BoostContainerManager persistentCallbacks');
_refreshOverlayEntries();
});
} else {
Logger.log('BoostContainerManager '+SchedulerBinding.instance.schedulerPhase.toString());
_refreshOverlayEntries();
}
fn();
//return super.setState(fn);
}
如果当前调度的是SchedulerPhase.persistentCallbacks,那么就加一个回调,在persistent之后进行调用_refreshOverlayEntries方法,
SchedulerPhase.persistentCallbacks 是处理build/layout/paint工作
可以这么理解,SchedulerPhase.persistentCallbacks就是在搭建舞台,舞台搭建好了,那么表演者就可以上台表演了 即调用_refreshOverlayEntries方法
继续查看_refreshOverlayEntries方法,代码如下
void _refreshOverlayEntries() {
final OverlayState overlayState = _overlayKey.currentState;
if (overlayState == null) {
return;
}
if (_leastEntries != null && _leastEntries.isNotEmpty) {
for (_ContainerOverlayEntry entry in _leastEntries) {
entry.remove();
}
}
final List<BoostContainer> containers = <BoostContainer>[];
containers.addAll(_offstage);
assert(_onstage != null, 'Should have a least one BoostContainer');
containers.add(_onstage);
//一层层的entry覆盖上去
_leastEntries = containers
.map<_ContainerOverlayEntry>(
(BoostContainer container) => _ContainerOverlayEntry(container))
.toList(growable: false);
overlayState.insertAll(_leastEntries);
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
final String now = _onstage.settings.uniqueId;
if (_lastShownContainer != now) {
final String old = _lastShownContainer;
_lastShownContainer = now;
_onShownContainerChanged(old, now);
}
//将焦点切换至当前的BoostContainerState
updateFocuse();
});
}
调用OverlayState的insertAll方法,将_leastEntries 覆盖上去,push flutter页面的讲解就到这人,pop其实也一样,将当前的页面栈弹出,当然也有特殊的业务处理,例如非当前的栈弹出,而是某个flutter页弹出,这里就不细讲,逻辑还是比较清晰好理解
其实本人在看完flutter的源码之后,对于BoostContainer.dart比较有疑问,其实是对其背后的对于Navigator和Overlay有疑问,BoostContainer要继承的是Navigator,这明明是个导航控制器,其实刚刚给出的文章里面讲得非常通俗易懂了。我自己疑问的原因主要是认为一个flutter app应该就只有一个Navigator,其实主要是flutter业务开发做多了而进去的误区。闲鱼的混合栈中的flutter页面栈管理就跟平常的flutter页面栈很不一样。其实最好的理解方式,自己写一个最简单的类似的flutter页面管理,然后再看那篇文章,就豁然开朗了。
容器讲解就到此了,解决疑问中的第一个问题
第一个:容器是怎么设计的?
适配层
适配层 只有原生才需要做相应的工作,看之前,想想如果要做适配层,要做哪些适配?
做过flutter业务开发,肯定在软键盘上面花过不少心思去做相应的界面适配工作~
的确,看原生代码里就有个XInputConnectionAdaptor.java的类,其实要看适配层,要花不少精力的,要弄清楚flutter的启动流程,然后重写FlutterView即XFlutterView,其实跟官方提供的FlutterView改动并不是很多
遗留问题:
因为 存在第一个打开的Flutter页面无法将数据传回起始页问题,
后来又去翻了下通道的相关代码,发现有这么一个flutter向原生的pageOnStart方法,类路径flutterProject/flutter_boost/android/src/main/java/com/idlefish/flutterboost/FlutterBoost.java,代码如下
case "pageOnStart":
{
Map<String, Object> pageInfo = new HashMap<>();
try {
IContainerRecord record = mManager.getCurrentTopRecord();
if (record == null) {
record = mManager.getLastGenerateRecord();
}
if(record != null) {
pageInfo.put("name", record.getContainer().getContainerUrl());
pageInfo.put("params", record.getContainer().getContainerUrlParams());
pageInfo.put("uniqueId", record.uniqueId());
}
result.success(pageInfo);
} catch (Throwable t) {
result.error("no flutter page found!",t.getMessage(),t);
}
}
break;
看了下代码,应该就是第一个flutter页面的打开逻辑,但是在flutter的demo中并没发现,可能是以前版本留下的