混合技术方案选型
FlutterBoost是一个Flutter插件,它的理念是将Flutter像Webview那样来使用,可以轻松地为现有原生应用程序提供Flutter混合集成方案。FlutterBoost采用共享引擎的实现方式,主要思路是由 Native容器Container 通过消息驱动 Flutter页面容器Container,从而达到 Native Container 与 Flutter Container 的同步目的。Flutter渲染的内容是由Native容器去渠道的。FlutterBoost帮助处理页面的映射和跳转,开发者只需关心页面的名字和参数即可(通常可以是URL)。它具有以下优点:
-
可复用通用型混合方案
-
支持更加复杂的混合模式,比如支持主页Tab
-
无侵入性方案:不再依赖修改Flutter的方案
-
支持通用页面生命周期
-
统一明确的设计概念
全局Router构建
在Native和Flutter混合开发项目中,首先我们需要解决的就是利用FlutterBoost构建一个全局Router,统一管理Native和Native、Native和Flutter,Flutter和Flutter之间页面跳转。
在Flutter Module中完成新页面的开发,需要在main.dart中定义相关路由,代码如下:
@override
void initState() {
super.initState();
FlutterBoost.singleton.registerPageBuilders(<String, PageBuilder>{
'/flutter/categoryFragment': (String pageName, Map<dynamic, dynamic> params, String _) => CategoryHomePage(),
'/flutter/brandFragment': (String pageName, Map<dynamic, dynamic> params, String _) => BrandPage(),
'/flutter/moreCategory' : (pageName, params, _) => MoreCategoryPage(ids: params['selectedIds'].cast<int>(),)
});
FlutterBoost.singleton.addBoostNavigatorObserver(TestBoostNavigatorObserver());
}
原生如何打开Flutter页面(Activity或者Fragment形式):通过BoostFlutterActivity和FlutterFragment来将Flutter页面打开。
具体实现:定义FlutterRouter类用来打开Flutter Activity和Fragment,其中 flutterRouter map对应main.dart中定义的所有路由,即flutterRouter map包含的路由通过FlutterBoost调用。在后面我们会将openPageByUrl添加原生路由的处理。
object FlutterRouter {
private val flutterRouter = mutableListOf(
FlutterPath.HOME_FRAGMENT,
FlutterPath.CATEGORY_FRAGMENT,
FlutterPath.BRAND_FRAGMENT,
FlutterPath.SEARCH_RESULT_FRAGMENT,
FlutterPath.USER_FRAGMENT,
)
@JvmOverloads
fun openPageByUrl(context: Context, url: String, orgParams: Map<String, Any> = emptyMap(), requestCode: Int = -1): Boolean {
//........
return try {
if (flutterRouter.contains(path)) {
// Flutter
val intent = BoostFlutterActivity.NewEngineIntentBuilder(BaseFlutterActivity::class.java).url(path).params(params)
.backgroundMode(BoostFlutterActivity.BackgroundMode.opaque).build(context)
if (context is Activity) {
context.startActivityForResult(intent, requestCode)
} else {
context.startActivity(intent)
}
return true
} else {
//Native
//.........
}
false
} catch (t: Throwable) {
LogUtils.e("openPageByUrl error: ${t.message}")
false
}
}
fun getFlutterFragment(url: String, params: Map<String, String> = emptyMap()): FlutterFragment {
val path = url.split("?")[0]
return if (flutterRouter.contains(url)) {
FlutterFragment.NewEngineFragmentBuilder().url(path).params(params).build()
} else {
FlutterFragment.NewEngineFragmentBuilder().url(FlutterPath.EMPTY_FRAGMENT).params(params).build()
}
}
}
在Flutter模块中页面跳转不使用Flutter原生提供的 Navigator实现跳转,而采用FlutterBoost来统一处理。FlutterBoost打开页面的方法为FlutterBoost.singleton.open,该方法会调用plugin中的openPage方法,而openPage最终调用到Native层中FlutterBoost初始化所定义的INativeRouter
Future<Map<dynamic, dynamic>> open(
String url, {
Map<String, dynamic> urlParams,
Map<String, dynamic> exts,
}) {
final Map<String, dynamic> properties = <String, dynamic>{};
properties['url'] = url;
properties['urlParams'] = urlParams;
properties['exts'] = exts;
return channel.invokeMethod<Map<dynamic, dynamic>>('openPage', properties);
}
//FlutterBoost初始化
val router = INativeRouter { context, url, urlParams, requestCode, exts ->
val assembleUrl = Utils.assembleUrl(url, urlParams)
FlutterRouter.openPageByUrl(context, assembleUrl, urlParams as Map<String, String>, requestCode)
}
val platform = FlutterBoost.ConfigBuilder(application, router)
.isDebug(AppConfig.DEBUG)
.whenEngineStart(FlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
.renderMode(FlutterView.RenderMode.texture)
.lifecycleListener(boostLifecycleListener)
.build()
FlutterBoost.instance().init(platform)
在INativeRouter实现中将路由处理也交给之前定义的FlutterRouter,完善openPageByUrl方法来统一处理所有路由,那么最终FlutterRouter可以用来处理所有Native和Flutter路由。
@JvmOverloads
fun openPageByUrl(context: Context, url: String, orgParams: Map<String, Any> = emptyMap(), requestCode: Int = -1): Boolean {
//........
return try {
if (flutterRouter.contains(path)) {
//Flutter
//..........
return true
} else {
//Native
when (path) {
IUserPath.ADDRESS_EDIT -> {
var address: Address? = null
try {
address = Gson().fromJson(params["address"].toString(), Address::class.java)
} catch (e: Exception) {
e.printStackTrace()
}
if (context is Activity) {
ARouterManager.getUserProvider().launchAddressEdit(context, address, params["isDefault"] as Boolean, requestCode)
}
}
else -> {
val postcard = ARouter.getInstance().build(path)
for ((key, value) in params) {
when (value) {
is Int -> postcard.withInt(key, value)
is Double -> postcard.withDouble(key, value)
is Long -> postcard.withLong(key, value)
else -> postcard.withString(key, value.toString())
}
}
postcard.navigation()
}
}
return true
}
false
} catch (t: Throwable) {
LogUtils.e("openPageByUrl error: ${t.message}")
false
}
}
上述代码中Native路由的处理针对某些路由进行了特殊处理,目的是为了兼容以前的代码。
全局Router参数处理
在路由跳转过程中,我们还需要关注如何传递和接收参数。参数主要包括基本类型、List数据、对象数据,下面分别介绍各种数据类型如何处理。
-
Native -> Flutter
从Native端参数会放入到SerializableMap中,然后传递给Flutter。在Flutter模块我们可以直接获得基本类型等,对象数据通过json传递获得
val ids = ArrayList<Int>()
ids.add(10)
ids.add(11)
val recent = LatelyGoods().apply {
productName = "AE80000"
price = 12.0
}
FlutterRouter.openPageByUrl(this@DebugActivity, FlutterPath.DEBUG_PAGE, orgParams = mapOf(
"name" to "zkh360",
"ids" to ids,
"objectStr" to Gson().toJson(recent)
))
//main.dart 中参数处理
DebugPage(name: params['name'], ids: params['ids'].cast<int>(), objectStr: params['objectStr'],)
//解析获得对象
var bean = JsonConvert.fromJsonAsT<RecentSkuEntity>(json.decode(widget.objectStr));
-
Flutter -> Native
如果是简单的Map参数的话,会直接通过
FlutterRouter中ARouter处理而不需要特殊处理。如果传递是对象的话会涉及到json转换,在我们项目中是为了保持原来业务代码而使用到对象数据的传递,但是建议后续参数全部采用Map形式传递,可以保证FlutterRouter处理的统一性
//简单Map参数传递, Native端不用特殊处理
FlutterBoost.singleton.open("/inventory/detail",
urlParams: {"inventoryId": inventoryId.toString()});
//将对象转化为json串
FlutterBoost.singleton.open("/user/person_edit", urlParams: {
Extras.PARAM_INVOICE: item == null ? "" : json.encode(item.toJson())})
//native端json数据处理
IUserPath.PERSON_EDIT -> {
var invoice: InvoiceBean? = null
try {
invoice = Gson().fromJson(params["invoice"].toString(), InvoiceBean::class.java)
} catch (e: Exception) {
e.printStackTrace()
}
if (context is Activity) {
ARouter.getInstance().build(IUserPath.PERSON_EDIT)
.withSerializable(Extras.PARAM_INVOICE, invoice)
.navigation(context, requestCode)
}
}
startActivityForResult处理
在Android中我们经常遇到这种场景:启动另一个Activity并接受返回的结果。那么在跨Module中怎么处理这种情况呢?
-
Native页面打开Flutter页面并获取返回数据
FlutterBoost通过
FlutterBoost.singleton.closeCurrent(result: result, exts: exts);方法来关闭页面,该方法会调用FlutterBoostPlugin中的closePage方法。
Future<bool> close(
String id, {
Map<String, dynamic> result,
Map<String, dynamic> exts,
}) {
// ....
return channel.invokeMethod<bool>('closePage', properties);
}
//FlutterBoostPlugin
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(), Log.getStackTraceString(t));
}
}
最终result 不为空就会调用setResult方法返回结果。其中对应result的key是RESULT_KEY = "_flutter_result_";
//FlutterActivityAndFragmentDelegate.java
@Override
public void finishContainer(Map<String, Object> result) {
if (result != null) {
setBoostResult(this.host.getActivity(), new HashMap<>(result));
this.host.getActivity().finish();
} else {
this.host.getActivity().finish();
}
}
public void setBoostResult(Activity activity, HashMap result) {
Intent intent = new Intent();
if (result != null) {
intent.putExtra(IFlutterViewContainer.RESULT_KEY, result);
}
activity.setResult(Activity.RESULT_OK, intent);
}
所以对于startActivityForResult启动的Flutter页面,只需要将返回值放入result map中即可。
//flutter给native回调
FlutterBoost.singleton.closeCurrent(result: { "selectedId": item.id, });
//native获得flutter回调参数
if (requestCode == Constants.REQUEST_CODE_ADDRESS_CHANGE) {
if (resultCode == Activity.RESULT_OK) {
try {
data?.let {
val map = it.getSerializableExtra(IFlutterViewContainer.RESULT_KEY) as HashMap<String, Any>
val addressId = map[Extras.PARAM_SELECTED_ID] as Int
}
}catch (e : Exception) {
LogUtils.e("AddressChangeError", e)
}
}
}
-
Flutter页面打开Native页面并获取返回数据
在Flutter中可以直接获取到打开页面之后的返回值,这里我们只需要关注返回值map中具体的key-value。
static void openAddressDetail(bool isDefault, AddressListEntity address,
{OnActivityResult onActivityResult}) {
boostOpen("/user/address_edit", urlParams: {
"isDefault": isDefault,
"address": address == null ? "" : json.encode(address.toJson()),
Extras.PARAM_REQUEST_CODE: 3000
}).then((Map<dynamic, dynamic> value) {
if (onActivityResult != null) {
onActivityResult(value[Constants.ResultCode] as int,
value[Constants.RequestCode] as int);
}
});
}
class Constants {
static const String ResultCode = "_resultCode__";
static const String RequestCode = "_requestCode__";
}
在FlutterBoost中Flutter页面是通过BoostFlutterActivity呈现,追踪源码可以看到最终返回值Map中requestCode和resultCode对应的key为_requestCode__和_resultCode__
//BoostFlutterActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
delegate.onActivityResult(requestCode, resultCode, data);
}
//FlutterActivityAndFragmentDelegate
public void onActivityResult(int requestCode, int resultCode, Intent data) {
mSyncer.onActivityResult(requestCode, resultCode, data);
Map<String, Object> result = null;
if (data != null) {
Serializable rlt = data.getSerializableExtra(RESULT_KEY);
if (rlt instanceof Map) {
result = (Map<String, Object>) rlt;
}
}
mSyncer.onContainerResult(requestCode, resultCode, result);
ensureAlive();
if (flutterEngine != null) {
Log.v(TAG, "Forwarding onActivityResult() to FlutterEngine:\\n"
+ "requestCode: " + requestCode + "\\n"
+ "resultCode: " + resultCode + "\\n"
+ "data: " + data);
flutterEngine.getActivityControlSurface().onActivityResult(requestCode, resultCode, data);
} else {
Log.w(TAG, "onActivityResult() invoked before NewFlutterFragment was attached to an Activity.");
}
}
//ContainerRecord
@Override
public void onContainerResult(int requestCode, int resultCode, Map<String, Object> result) {
mManager.setContainerResult(this, requestCode,resultCode, result);
}
//FlutterViewContainerManager
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);
}
}
MethodChannel交互
Flutter平台特定的API支持不依赖于代码生成,而是依赖于灵活的消息传递的方式:
- 应用的Flutter部分通过平台通道(platform channel)将消息发送到其应用程序的所在的宿主(iOS或Android)
- 宿主监听的平台通道,并接收该消息。然后它会调用特定于该平台的API(使用原生编程语言)并将响应发送回客户端,即应用程序的Flutter部分
在FlutterBoost初始化的时候,可以在BoostLifecycleListener中创建MethodChannel来提供与客户端通信的渠道
在FlutterBoost中创建MethodChannel
//FlutterBoost初始化
val boostLifecycleListener: BoostLifecycleListener = object : BoostLifecycleListener {
override fun beforeCreateEngine() {}
override fun onEngineCreated() {
val messenger: BinaryMessenger = FlutterBoost.instance().engineProvider().dartExecutor
MethodChannel(messenger, "zkh_method_channel").apply {
setMethodCallHandler(ZKHMethodCallHandler())
}
}
override fun onPluginsRegistered() {}
override fun onEngineDestroy() {}
}
val platform = FlutterBoost.ConfigBuilder(application, router)
.isDebug(AppConfig.DEBUG)
.whenEngineStart(FlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
.renderMode(FlutterView.RenderMode.texture)
.lifecycleListener(boostLifecycleListener)
.build()
FlutterBoost.instance().init(platform)
宿主(iOS或Android)代码实现
class ZKHMethodCallHandler : MethodChannel.MethodCallHandler {
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
val systemInfo = CommonHeaderInterceptor.SystemInfo(ZApplication.getContext(), "Android")
LogUtils.e("call.method=${call.method}; arguments=${call.arguments}")
when (call.method) {
"getLocalData" -> {
when(call.argument<String>("key")) {
"userId" -> result.success(SPUtils.getUserId())
}
}
"setLocalData" -> {
when(call.argument<String>("key")) {
"accessToken" -> SPUtils.putAccessToken(call.argument<String>("value"))
}
}
else -> result.notImplemented()
}
}
}
Flutter部分调用平台通道
static const MethodChannel _channel = MethodChannel('zkh_method_channel');
/// 获得本地sp数据
static Future<T> getLocalData<T>(String key) async {
final T value = await _channel.invokeMethod('getLocalData', {
'key' : key
});
return value;
}
/// 设置本地sp数据
static Future<Void> setLocalData<T>(String key, T value) async {
await _channel.invokeMethod('setLocalData', {
'key':key,
'value':value
});
return null;
}