uni-app 在 flutter 应用中的应用实践。

2,676 阅读6分钟

混合应用大杂烩(flutter + uniapp)

初衷

一直以来很多项目中使用了不少跨平台开发方案,从react-native到weex再到flutter。使用下来感受最优的方案是 flutter,在短暂的发展过程中出现了一大批优秀的开源组件。由于项目开发初期选型 flutter 项目中 80%的功能使用 flutter 开发。后来项目功能不断增加,和项目的小程序端功能出现重复。flutter 中集成 uniapp 的需求就产生了。

功能

  1. flutter 拉起指定uniapp并可携带参数实现指定页面跳转
  2. uniapp 版本热更新
  3. uniapp 使用flutter编写的通用组件并可以透传数据给uniapp(避免uniapp引用部分无用的原生模块)

废话不多说上代码

Android

原生代码

// 小程序Event 对象
val event = EventChannel(messenger, "flutter_uni_stream")
var eventSink: EventSink? = null
event.setStreamHandler(
    object : StreamHandler {
        override fun onListen(arguments: Any?, events: EventSink) {
            eventSink = events
            Log.d("Android", "EventChannel onListen called")
        }

        override fun onCancel(arguments: Any?) {
            Log.w("Android", "EventChannel onCancel called")
        }
    })

// 小程序实例
val unimpMap = mutableMapOf<String?, IUniMP?>();

var uniMpcallback:DCUniMPJSCallback? = null

// 小程序Channel 对象
val channel = MethodChannel(messenger, "flutter_uni")
channel.setMethodCallHandler { call, res ->
    // 根据方法名,分发不同的处理
    when (call.method) {
        "initMP" -> {
            try {
                if (DCUniMPSDK.getInstance().isInitialize()) {
                    res.success(2)
                } else {
                    // 初始化uniMPSDK
                    val config = DCSDKInitConfig.Builder()
                        .setCapsule(false)
                        .build()

                    DCUniMPSDK.getInstance().initialize(this, config)

                    //监听胶囊点击事件
                    DCUniMPSDK.getInstance()
                        .setCapsuleMenuButtonClickCallBack { argumentAppID ->
                            val backdata = JSONObject().apply {
                                set("appid", argumentAppID)
                                set("event", "capsuleaction")
                            }
                            eventSink?.success(backdata)
                        }

                    // 监听小程序关闭
                    DCUniMPSDK.getInstance().setUniMPOnCloseCallBack { argumentAppID ->
                        if (unimpMap.containsKey(argumentAppID)) {
                            unimpMap.remove(argumentAppID)
                            unimpMap[argumentAppID]?.closeUniMP();
                        }
                        val backdata = JSONObject().apply {
                            set("appid", argumentAppID)
                            set("event", "close")
                        }
                        eventSink?.success(backdata)
                    }

                    //监听小程序向原生发送事件回调方法
                    DCUniMPSDK.getInstance()
                        .setOnUniMPEventCallBack { argumentAppID, event, data, callback ->
                            val backdata = JSONObject().apply {
                                set("appid", argumentAppID)
                                set("event", event)
                                set("data", data)
                            }
                            eventSink?.success(backdata)
                            uniMpcallback = callback
                        }

                    res.success(1)
                }
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        /** 检查指定的 UniMP 小程序
         * {
         *      "appid": ""
         * }
         */
        "checkMP" -> {
            try {
                // 接收 Flutter 传入的参数
                val argumentAppID: String? = call.argument<String>("appid")
                if (DCUniMPSDK.getInstance().isExistsApp(argumentAppID)) {
                    res.success(true)
                } else {
                    res.success(false)
                }
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        /** 获取指定的 UniMP 小程序版本
         * {
         *      "appid": ""
         * }
         */
        "versionMP" -> {
            try {
                // 接收 Flutter 传入的参数
                val argumentAppID: String? = call.argument<String>("appid")
                val result = DCUniMPSDK.getInstance().getAppVersionInfo(argumentAppID)
                var versionStr: String? = null;
                if(result != null) {
                    versionStr = result.toString()
                }
                res.success(versionStr)
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        /** 安装 UniMP 小程序
         * {
         *      "appid": "",
         *      "wgtPath": ""
         * }
         */
        "installMP" -> {
            try {
                // 接收 Flutter 传入的参数
                val argumentAppID: String? = call.argument<String>("appid")
                val wgtPath: String? = call.argument<String>("wgtPath")
                val releaseConfig = UniMPReleaseConfiguration()
                releaseConfig.wgtPath = wgtPath
                DCUniMPSDK.getInstance().releaseWgtToRunPath(
                    argumentAppID,
                    releaseConfig
                ) { code, result ->
                    if (code == 1) {
                        res.success(true)
                    } else {
                        res.success(false)
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        /** 打开指定的 UniMP 小程序
         * {
         *      "appid": "",
         *      "isreload": true //重新打开
         *      "config": {
         *          "extraData": {},  //其他自定义参数JSON
         *          "path": "" //指定启动应用后直接打开的页面路径
         *      }
         * }
         */
        "openMP" -> {
            try {
                // 接收 Flutter 传入的参数
                val argumentAppID: String? = call.argument<String>("appid")
                if (unimpMap.containsKey(argumentAppID) == false) {
                    val argumentConfig: HashMap<String,Any>? = call.argument<HashMap<String,Any>>("config")
                    val uniMPOpenConfiguration = UniMPOpenConfiguration()
                    if (argumentConfig != null && argumentConfig.containsKey("extraData")) {
                        val jsonObject = org.json.JSONObject()
                        var extraData = argumentConfig.get("extraData") as HashMap<String,Any>
                        extraData.forEach { s, any ->  jsonObject.put(s,any) }
                        jsonObject.put("path",argumentConfig.get("path") as String?)
                        uniMPOpenConfiguration.extraData = jsonObject
                    }
                    if (argumentConfig != null && argumentConfig.containsKey("path")) {
                        uniMPOpenConfiguration.path = argumentConfig.get("path") as String?
                    }
                    // 打开小程序
                    unimpMap[argumentAppID] = DCUniMPSDK.getInstance()
                        .openUniMP(applicationContext, argumentAppID, uniMPOpenConfiguration)
                    res.success(true)
                } else {
                    val data = call.argument<Any>("config")
                    val backdata = JSONObject().apply {
                        set("appid", argumentAppID)
                        set("data", data)
                    }
                    unimpMap[argumentAppID]?.sendUniMPEvent("open_app", backdata)
                    unimpMap[argumentAppID]?.showUniMP();
                    res.success(true)
                }
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        /** 隐藏指定的 UniMP 小程序
         * {
         *      "appid": "",
         * }
         */
        "hideMP" -> {
            try {
                // 接收 Flutter 传入的参数
                val argumentAppID: String? = call.argument<String>("appid")
                if (unimpMap.containsKey(argumentAppID)) {
                    unimpMap[argumentAppID]?.hideUniMP();
                }
                res.success(true)
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        /** 关闭指定的 UniMP 小程序
         * {
         *      "appid": "",
         * }
         */
        "closeMP" -> {
            try {
                // 接收 Flutter 传入的参数
                val argumentAppID: String? = call.argument<String>("appid")
                if (unimpMap.containsKey(argumentAppID)) {
                    unimpMap.remove(argumentAppID)
                    unimpMap[argumentAppID]?.closeUniMP();
                }
                res.success(true)
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        /**发送数据到指定的UniMP小程序
         * {
         *      "appid": "",
         *      "event": "",
         *      "data": {}
         * }
         */
        "sendMP" -> {
            try {
                // 接收 Flutter 传入的参数
                val argumentAppID = call.argument<String>("appid")
                val sendEvent = call.argument<String>("event")
                val data = call.argument<Any>("data")

                val backdata = JSONObject().apply {
                    set("appid", argumentAppID)
                    set("event", sendEvent)
                    set("data", data)
                }
                unimpMap[argumentAppID]?.sendUniMPEvent(sendEvent, backdata)
                res.success(true)
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        /** 回调数据到到指定的UniMP小程序
         * {
         *      "appid": "",
         *      "event": "",
         *      "data": {}
         * }
         */
        "callbackMP" -> {
            try {
                // 接收 Flutter 传入的参数
                val argumentAppID = call.argument<String>("appid")
                val sendEvent = call.argument<String>("event")
                val data = call.argument<Any>("data")

                val backdata = JSONObject().apply {
                    set("appid", argumentAppID)
                    set("event", sendEvent)
                    set("data", data)
                }
                uniMpcallback?.invoke(backdata)
                res.success(true)
            } catch (e: Exception) {
                e.printStackTrace()
                res.error("error_code", e.message, e.printStackTrace().toString())
            }
        }

        else -> {
            // 如果有未识别的方法名,通知执行失败
            res.error("error_code", "error_message", null)
        }
    }
}

ios

原生代码

///小程序打开Map
  var uniMpMap: [String: DCUniMPInstance] = [:]
  ///监听sink
  var eventSink:FlutterEventSink?
  ///回调函数
  var uniMpCallback:DCUniMPKeepAliveCallback?
  
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    ///是否初始化
    var isInit = false
    
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(name: "flutter_uni",binaryMessenger: controller.binaryMessenger)
    let event = FlutterEventChannel(name: "flutter_uni_stream",binaryMessenger: controller.binaryMessenger)
    event.setStreamHandler(self)
    
    channel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      switch(call.method) {
      case "initMP":
        if isInit {
          result(2)
        } else {
          let options = NSMutableDictionary.init(dictionary: launchOptions ?? [:])
          options.setValue(NSNumber.init(value: true), forKey: "debug")
          DCUniMPSDKEngine.setDelegate(self!)
          DCUniMPSDKEngine.initSDKEnvironment(launchOptions: options as! [AnyHashable : Any])
          DCUniMPSDKEngine.setCapsuleButtonHidden(true)
          isInit = true
          result(1)
        }
        break
      case "checkMP":
        if let arguments = call.arguments as? Dictionary<String, Any> {
          let appid: String = arguments["appid"] as? String ?? ""
          if DCUniMPSDKEngine.isExistsUniMP(appid) {
            result(true)
          } else {
            do {
              let wgtPath = Bundle.main.path(forResource: appid, ofType: "wgt") ?? ""
              try DCUniMPSDKEngine.installUniMPResource(withAppid: appid, resourceFilePath: wgtPath, password: nil)
              result(true)
            } catch {
              result(false)
            }
          }
        }
        break
      case "versionMP":
        if let arguments = call.arguments as? Dictionary<String, Any> {
          let appid: String = arguments["appid"] as? String ?? ""
          let versionInfo = DCUniMPSDKEngine.getUniMPVersionInfo(withAppid: appid)
          result(versionInfo)
        }
        result(false)
        break
      case "installMP":
        if let arguments = call.arguments as? Dictionary<String, Any> {
          let appid: String = arguments["appid"] as? String ?? ""
          let wgtPath: String = arguments["wgtPath"] as? String ?? ""
          do {
            try DCUniMPSDKEngine.installUniMPResource(withAppid: appid, resourceFilePath: wgtPath, password: nil)
            result(true)
          } catch {
            result(false)
          }
        }
        result(false)
        break
      case "openMP":
        if let arguments = call.arguments as? Dictionary<String, Any> {
          let appid: String = arguments["appid"] as? String ?? ""
          if(self!.uniMpMap[appid] != nil) {
            var backdata: [String: Any] = [:]
            backdata["appid"] = appid
            backdata["data"] = arguments["config"]
            self!.uniMpMap[appid]?.sendUniMPEvent("open_app", data: backdata)
            self!.uniMpMap[appid]?.show {(success, error) in
              if success {
                result(true)
              } else {
                result(false)
              }
            }
          } else {
            let data: [String: Any] = (arguments["config"] as? [String: Any])!
            let configuration = DCUniMPConfiguration.init()
            configuration.enableBackground = true
            configuration.enableGestureClose = true
            if let extraData = data["extraData"] as? [String: Any], let path = data["path"] {
                var updatedExtraData = extraData
                updatedExtraData["path"] = path
                configuration.extraData = updatedExtraData
            }

            if let path = data["path"] {
                configuration.path = path as? String
            }
            DCUniMPSDKEngine.openUniMP(appid, configuration: configuration) { instance, error in
              if instance != nil {
                self!.uniMpMap[appid] = instance;
                result(true)
              } else {
                result(false)
              }
            }
          }
        }
        break
      case "hideMP":
        if let arguments = call.arguments as? Dictionary<String,Any> {
          let appid: String = arguments["appid"] as? String ?? ""
          if(self!.uniMpMap[appid] != nil) {
            self!.uniMpMap[appid]?.hide { (success, error) in
              if success {
                result(true)
              } else {
                result(false)
              }
            }
          }
        }
        result(true)
        break
      case "closeMP":
        if let arguments = call.arguments as? Dictionary<String,Any> {
          let appid: String = arguments["appid"] as? String ?? ""
          if(self!.uniMpMap[appid] != nil) {
            self!.uniMpMap[appid]?.close { (success, error) in
              if success {
                self!.uniMpMap.removeValue(forKey: appid)
                result(true)
              } else {
                result(false)
              }
            }
          }
        }
        result(false)
        break
      case "sendMP":
        if let arguments = call.arguments as? Dictionary<String,Any> {
          let appid: String = arguments["appid"] as? String ?? ""
          let event: String = arguments["event"] as? String ?? ""
          let data: Any = arguments["data"] ?? [:]
          if(self!.uniMpMap[appid] != nil) {
            var backdata: [String: Any] = [:]
            backdata["appid"] = appid
            backdata["event"] = event
            backdata["data"] = data
            self!.uniMpMap[appid]?.sendUniMPEvent(event, data: backdata)
            result(true)
          }
        }
        result(false)
        break
      case "callbackMP":
        if let arguments = call.arguments as? Dictionary<String,Any> {
          let appid: String = arguments["appid"] as? String ?? ""
          let event: String = arguments["event"] as? String ?? ""
          let data: Any = arguments["data"] ?? [:]
          var backdata: [String: Any] = [:]
          backdata["appid"] = appid
          backdata["event"] = event
          backdata["data"] = data
          if let callback = self?.uniMpCallback {
            callback(backdata,true)
          }
        }
        result(false)
        break
      default:
        result(FlutterMethodNotImplemented)
        break
      }
    })
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
  ///FlutterStreamHandler监听
  func onListen(withArguments arguments: Any?,
                eventSink event: @escaping FlutterEventSink) -> FlutterError? {
    eventSink = event
    return nil
  }
  
  ///FlutterStreamHandler监听
  func onCancel(withArguments arguments: Any?) -> FlutterError? {
    eventSink = nil
    return nil
  }
  
  ///生命周期
  override func applicationDidBecomeActive(_ application: UIApplication) {
    DCUniMPSDKEngine.applicationDidBecomeActive(application)
  }
  
  override func applicationWillResignActive(_ application: UIApplication) {
    DCUniMPSDKEngine.applicationWillResignActive(application)
  }
  
  override func applicationDidEnterBackground(_ application: UIApplication) {
    DCUniMPSDKEngine.applicationDidEnterBackground(application)
  }
  
  override func applicationWillEnterForeground(_ application: UIApplication) {
    DCUniMPSDKEngine.applicationWillEnterForeground(application)
  }
  
  override func applicationWillTerminate(_ application: UIApplication) {
    DCUniMPSDKEngine.destory()
  }
  
  ///监听小程序向原生发送事件回调方法
  func onUniMPEventReceive(_ appid: String, event: String, data: Any, callback: @escaping DCUniMPKeepAliveCallback) {
    var backdata: [String: Any] = [:]
    backdata["appid"] = appid
    backdata["event"] = event
    backdata["data"] = data
    eventSink?(backdata)
    uniMpCallback = callback
  }
  
  ///监听胶囊点击事件
  func hookCapsuleMenuButtonClicked(_ appid: String) {
    var backdata: [String: Any] = [:]
    backdata["appid"] = appid
    backdata["event"] = "capsuleaction"
    eventSink?(backdata)
  }
  
  ///监听小程序关闭
  func hookCapsuleCloseButtonClicked(_ appid: String) {
    var backdata: [String: Any] = [:]
    backdata["appid"] = appid
    backdata["event"] = "close"
    eventSink?(backdata)
  }

flutter

import 'dart:async';
import 'package:flutter/services.dart';

class UniMP {
  static const MethodChannel _channel = MethodChannel('flutter_uni');
  static const EventChannel _stream = EventChannel('flutter_uni_stream');

  static StreamController? _controller;
  static Stream<dynamic>? _streamInstance;

  static Stream<dynamic>? get uniStream {
    if (_streamInstance == null) {
      _controller = StreamController.broadcast();
      _streamInstance = _controller!.stream;
      _stream.receiveBroadcastStream().listen((event) {
        _controller!.add(event);
      }, onError: (err) {
        _controller!.addError(err);
      });
    }
    return _streamInstance;
  }

  /// 初始化uniMPSDK
  static Future<dynamic> init({void Function(dynamic)? receive}) async {
    uniStream?.listen((event) async{
      if(receive != null) {
        receive(event);
      }
    }, onError: (err) {
      print('Error occurred: $err');
    });
    final result = await _channel.invokeMethod('initMP');
    return result;
  }

  /// 检查指定的 UniMP 小程序
  static Future<dynamic> checkMP({required String appid}) async {
    final result = await _channel.invokeMethod('checkMP', {'appid': appid});
    return result;
  }

  /// 获取指定的 UniMP 小程序版本
  static Future<dynamic> versionMP({required String appid}) async {
    final result = await _channel.invokeMethod('versionMP', {'appid': appid});
    return result;
  }

  /// 安装 UniMP 小程序
  static Future<dynamic> installMP({required String appid,required String wgtPath}) async {
    final result = await _channel.invokeMethod('installMP', {'appid': appid,"wgtPath": wgtPath});
    return result;
  }

  /// 打开指定的 UniMP 小程序
  static Future<dynamic> openMP({
    required String appid,
    Map<String, dynamic>? config,
  }) async {
    final result = await _channel.invokeMethod('openMP', {
      'appid': appid,
      'config': config ?? null,
    });
    return result;
  }

  /// 关闭指定的 UniMP 小程序
  static Future<void> closeMP({required String appid}) async {
    final result = await _channel.invokeMethod('closeMP', {'appid': appid});
    return result;
  }

  /// 隐藏指定的 UniMP 小程序
  static Future<void> hideMP({required String appid}) async {
    final result = await _channel.invokeMethod('hideMP', {'appid': appid});
    return result;
  }

  /// 发送数据到指定的UniMP小程序
  static Future<void> sendMP({
    required String appid,
    required String event,
    Map<String, dynamic>? data,
  }) async {
    final result = await _channel.invokeMethod('sendMP', {
      'appid': appid,
      'event': event,
      'data': data ?? {},
    });
    return result;
  }

  /// 回调数据到到指定的UniMP小程序
  static Future<void> callbackMP({
    required String appid,
    required String event,
    Map<String, dynamic>? data,
  }) async {
    final result = await _channel.invokeMethod('callbackMP', {
      'appid': appid,
      'event': event,
      'data': data ?? {},
    });
    return result;
  }
}

接下来说咋使用

  1. flutter
//初始化小程序
await UniMP.init(receive: (event) async{
  //文件选择
  if(event['event'] == 'choose_file') {
    UniMP.hideMP(appid: event['appid']);
    Map data = event['data'];
    
    // TODO编写的文件选择功能
    List files = [];
    
    UniMP.callbackMP(appid: event['appid'], event: event['event'],data: {"files": files});
    UniMP.openMP(appid: event['appid']);
  }
  
  //TODO 想使用uniapp拉起flutter 自己定 event 即可
});



//检测更新安装
var versionResult = await UniMP.versionMP(appid: "__UNI__1946C50");
if(versionResult != null) {
  Map version = jsonDecode(versionResult);
  // TODO判断是否需要更新,下载保存 wgt 包。指定存储的wgtPath地址,执行安装更新(uniapp版本 code必须增加才能更新成功)
  await UniMP.installMP(appid: "__UNI__888888", wgtPath: wgtPath);
}

//打开并携带参数指定路径
await UniMP.openMP(appid: "__UNI__888888",config: {"extraData": {},"path":"/pages/base/index"});

2.uniapp

//App.vue launch代码
async onLaunch(options) {
	plus.nativeUI.toast = function (str) {
		if (str == "再按一次退出应用") {
		        plus.runtime.quit()
		    } else {
		        uni.showToast({
		            title: str,
		            icon: "none"
		        })
		    }
		}

        uni.onNativeEventReceive((event, data) => {
            if (event == "open_app") {
                var pages = getCurrentPages()
                var page = pages[pages.length - 1]
                if (page.$getAppWebview()["__uniapp_route"] != data["data"]["path"]) {
                    uni.reLaunch({
                        url: data["data"]["path"]
                    })
                }
            }
        })
}
    
    
//uniapp 拉起flutter 文件上传功能
uni.sendNativeEvent(
    "choose_file",
    {
       type: "audio"
    },
    async function (ret) {
      if (ret["data"]["files"].length > 0) {
           //TODO执行上传动作                 
       }
    }
);

结尾

完整示例就不开源了,懒得梳理哈哈。现有项目依赖太多整理一个完整的太麻烦了。引入 uniapp sdk 可以查看官方文档,这里我也不在赘述了。如有啥问题欢迎留言交流。