FlutterBoost Android混合开发实践

1,816 阅读5分钟

公众号原文地址

混合技术方案选型

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形式):通过BoostFlutterActivityFlutterFragment来将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数据、对象数据,下面分别介绍各种数据类型如何处理。

  1. 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));
  1. 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中怎么处理这种情况呢?

  1. 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)
       }
    }
}
  1. 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中requestCoderesultCode对应的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;
  }