Dart 层如何 兼容 Android 和iOS平台特性

1,416 阅读6分钟

Dart 层如何 兼容 Android 和iOS平台特性

为什么要 兼容Android 和ISO

  • 由于 Flutter 只接管了应用渲染层,因此这些系统底层能力是无法在 Flutter 框架内提供支持的;
  • Flutter 还是一个相对年轻的生态,因此原生开发中一些相对成熟的 Java、C++ 或 Objective-C 代码库,比如图片处理、音视频编解码等,可能在 Flutter 中还没有相关实现。

flutter 为我们提供了方法通道(MethodChannel)来完成,flutter 对原生的调用。

整个调用的流程和网络请求类似:

  1. 作为客户端的 flutter,通过方法通道发起方法调用请求;
  2. 作为服务端的原生,通过方法通道 接收到了 请求,然后调用原生 api 处理好请求
  3. 再通过方法通道,将处理好的通过方法通道 回传到 flutter 客户端
  4. flutter 接手到处理请求的结果,判断是否成功,然后进行之后的逻辑处理

下面看看,打开微信应用的小例子:

flutter 端代码

class FlutterCallNativeDemo extends StatelessWidget {
  // 声明方法通道
  var platform = MethodChannel("samples.chenhang/utils");

  //处理点击事件
  openWeChat() async {
    int result;
    try {
      // 通过方法调用,调用原生方法
      result = (await platform.invokeListMethod("openWeChat")) as int;
    } catch (e) {
      print("result error=$e");
      result = -1;
    }
    print("result=$result");
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("flutter 调用原生"),
      ),
      body: Center(
        child: Column(
          children: [
            MaterialButton(
              onPressed: () {
                openWeChat();
              },
              child: Text("打开微信"),
            )
          ],
        ),
      ),
    );
  }
}

Android 端:找到 Flutter 应用的入口,也就是在 MainActivity 中的 FlutterView 里实现的。打开项目下的android 目录,找到 MainActivity 文件,在 onCreate(savedInstanceState: Bundle?)注意是在只有一个参数的 onCreate 方法里面。

class MainActivity : FlutterActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        flutterEngine?.let {
            GeneratedPluginRegistrant.registerWith(it)
        }

        MethodChannel(flutterEngine?.dartExecutor?.binaryMessenger, "samples.chenhang/utils").setMethodCallHandler { call, result ->

            if (call.method == "openWeChat") {
                try {
                    val weChatPackageName = "com.tencent.mm"
                    if (isApplicationAvilible(weChatPackageName)) {
                        val intent = Intent(Intent.ACTION_MAIN)
                        intent.addCategory(Intent.CATEGORY_LAUNCHER)
                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        val cn = ComponentName(weChatPackageName, "com.tencent.mm.ui.LauncherUI")
                        intent.component = cn
                        startActivity(intent)
                    } else {
                        Toast.makeText(this, "您没有安装微信,请先安装", Toast.LENGTH_SHORT).show()
                    }
                    result.success(0)
                } catch (e: Exception) {
                    result.error("UNAVAILABLE", "您没有安装微信", null)
                }

            } else {
                result.notImplemented()
            }
        }
    }

    private fun isApplicationAvilible(packageName: String): Boolean {
        val installedPackages = packageManager.getInstalledPackages(0)
        if (installedPackages != null) {
            for (packageInfo in installedPackages) {
                if (packageInfo.packageName == packageName) {
                    return true
                }
            }
        }
        return false
    }

}

到此flutter 调用 Android 就完成了,看起来还是挺简单的。

由于没有iPhone 真机,模拟器上没有安装 微信,就以打开浏览器为例子吧:

@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController;
    // 1. 声明方法通道
    let batteryChannel = FlutterMethodChannel.init(name: "samples.chenhang/utils",binaryMessenger: controller.binaryMessenger);
        batteryChannel.setMethodCallHandler({
            //2.处理flutter 的请求
            (call: FlutterMethodCall, result: FlutterResult)  -> Void in
            if(call.method=="openWeChat"){
                // 处理方法名为 openWeChat的请求
                self.openWeChat()
                result(0)
            }else{
                result(FlutterMethodNotImplemented)
            }
          // Handle battery messages.
        });
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    // 调用原生打开浏览器
    func openWeChat() {
        let urlString = "https://flutterchina.club/platform-channels/"
        let url = URL(string: urlString)
        if #available(iOS 10.0, *) {
            UIApplication.shared.open(url!, options: [:], completionHandler: nil)
        } else {
            // Fallback on earlier versions
        }
    }
}

找到 找到 Flutter 应用的入口,也就是在 AppDelegate,然后在 application 方法中进行 flutter 的请求处理。

flutter 如何使用原生视图

通过方法通道,我们可以把原生操作系统提供的底层能力,以及现有原生开发中一些相对成熟的解决方案,以接口封装的形式在 Dart 层快速搞定,从而解决原生代码在 Flutter 上的复用问题。然后,我们可以利用 Flutter 本身提供的丰富控件,做好 UI 渲染。

是不是只要通过方法通道就完成了APP开发中的所有事情呢?显然并不是这样的,下面我们一起来看看复杂APP的构建都有什么组成。

image.png 通过flutter 和方法通道,我们可以解决了应用层能力、应用层渲染、底层能力但是 对于那些涉及到底层渲染,比如浏览器、相机、地图,以及原生自定义视图的场景,自己在 Flutter 上重新开发一套显然不太现实。

Flutter 提供了一个平台视图(Platform View)的概念。它提供了一种方法,允许开发者在 Flutter 里面嵌入原生系统(Android 和 iOS)的视图,并加入到 Flutter 的渲染树中,实现与 Flutter 一致的交互体验。

创建平台视图的流程:

  • 首先,由作为客户端的 Flutter,通过向原生视图的 Flutter 封装类(在 iOS 和 Android 平台分别是 UIKitView 和 AndroidView)传入视图标识符,用于发起原生视图的创建请求;
  • 然后,原生代码侧将对应原生视图的创建交给平台视图工厂(PlatformViewFactory)实现;
  • 最后,在原生代码侧将视图标识符与平台视图工厂进行关联注册,让 Flutter 发起的视图创建请求可以直接找到对应的视图创建工厂。

下面就以一个 使用 Android TextView 和iOS的 UIText 为例子:

flutter 端

// 加载原生视图,Android 使用:AndroidView;ios 使用UiKitView
class NativeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(viewType: "com.example.flutter_base_app/textView");
    } else {
      return UiKitView(
        viewType: "com.example.flutter_base_app/textView",
      );
    }
  }
}

class NativeWidgetDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("显示原生控件"),
      ),
      body: Center(
        child: NativeWidget(),
      ),
    );
  }
}

非常简单,只需要根据不同平台,创建不同的widget 即可;Android 为AndroidView;ios 为UiKitView。

Android:

  1. 构建返回的view 继承 PlatformView
class NativeTextView(val context: Context?, val message: BinaryMessenger) : PlatformView, MethodChannel.MethodCallHandler {
    // 传入 BinaryMessenger 只是为了创建 MethodChannel使用
    private var textView: TextView? = null
    private val updateText = "update_text"

    init {
        textView = TextView(context)
        textView?.text = "我是来自Android的textView"
        val methodChannel = MethodChannel(message, "")
        methodChannel.setMethodCallHandler(this)
    }

    override fun getView(): View {
        return textView!!
    }

    override fun dispose() {
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            updateText -> {

            }
        }

    }

}
  1. 创建 视图工厂 实现
class NativeViewFactory(private val message: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context?, viewId: Int, args: Any?): PlatformView {
        return NativeTextView(context = context, message = message)
    }

}
  1. 自定义Plugin类,注册视图工厂
object NativeViewPlugin {
    private val viewTypeId = "com.example.flutter_base_app/textView"

    fun registerWith(flutterEngine: FlutterEngine?) {
        flutterEngine?.let {
            val key = NativeViewPlugin::class.java.canonicalName
            val shimPluginRegistry = ShimPluginRegistry(flutterEngine);
            if (shimPluginRegistry.hasPlugin(key)) return
            val registry = shimPluginRegistry.registrarFor(key)
            registry.platformViewRegistry().registerViewFactory(viewTypeId, NativeViewFactory(registry.messenger()))
        }

    }
}

iOS

  1. 在 info.plist 添加 io.flutter.embedded_views_preview= true

image.png 2. 创建返回的视图类


import Foundation

import Foundation
import Flutter
class PlatformTextView: NSObject,FlutterPlatformView {
    let frame: CGRect;
    let viewId: Int64;
    var text:String = ""

    init(_ frame: CGRect,viewID: Int64,args :Any?) {
        self.frame = frame
        self.viewId = viewID
//        if(args is NSDictionary){
//            let dict = args as! NSDictionary
//            self.text = dict.value(forKey: "text") as! String
//        }
    }
    func view() -> UIView {
        let label = UILabel()
        label.text = "我是来自iOS的文本"
        label.textColor = UIColor.red
        label.frame = self.frame
        return label
    }
}
  1. 创建视图工厂类
import Foundation
import Flutter

class PlatformTextViewFactory: NSObject,FlutterPlatformViewFactory {
    func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
        //返回一个视图控件
        return PlatformTextView(frame,viewID: viewId,args: args)
    }
    func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
        return FlutterStandardMessageCodec.sharedInstance()
    }
}
  1. 在 AppDelegate的 application 中注册工厂视图
   //通过平台视图,把原生控件返回给flutter层
    func testPlatformView() {
        let factory = PlatformTextViewFactory()
        let registrar = self.registrar(forPlugin: "platform_text_view_plugin")//这里的插件名可以随便取
        registrar?.register(factory, withId: "com.example.flutter_base_app/textView")//这里的withId和flutter 中定义的 viewType 要保持一致
    }

总结

通过方法通道 MethodChannel 完成了在dark 层面对原生的方法调用;在通过平台视图,获取到原生的视图,然后添加到widget 树,完成渲染。通过这两种方式就完成了dark 层对原生的调用,使得APP有了底层能力和底层渲染的功能,完成复杂APP的构建。

参考链接

blog.csdn.net/sinat_17775… blog.csdn.net/weixin_3615… www.jianshu.com/p/8d74a7318…