Loading...
墨滴

ThatcherLi

2021/05/06  阅读:153  主题:默认主题

Flutter | 如何优雅的调用 Android 原生方法?

Flutter 的优势与缺点

Flutter 作为一个跨平台的移动 UI 框架,可以快速在 iOS 和 Android 上构建高质量的原生用户界面。可以说是一套代码做到了多端运行,我们常说的 Flutter 跨平台,其实是 UI 跨平台,编写好了一套 UI 代码,就可以在 IOS、Android、Web 上呈现出同样的效果,节省了大量的人力成本,这也是 Flutter 越来越流行的原因之一。

然而 Flutter 也有它的缺点,很明显的是它并不能直接调用平台的系统功能,比如使用蓝牙、相机、GPS、音量、电池等,因此要在 Flutter 中调用这些能力就必须和原生平台进行通信。

Flutter 架构概览

了解一个框架的全貌,有助于我们从更高的视角去看待一门新技术,而避免深陷代码细节,无法自拔。首先来看一张官方给出的 Flutter 整体架构图,如下。

Flutter 架构概览
Flutter 架构概览

官方将 Flutter 框架大致分为了三层,Framework、Engine、Embedder。下面我将详细讲解一下这三层到底是什么,在实际开发中承担着怎样的角色。

Dart Framework 层

作为一个 Flutter 开发者,辛勤的码农,你每天都在这片土地挥洒着汗水。 你每天使用的各种 UI 组件(Flutter UI 控件已经快接近 400 个了),各种 Flutter Package,Flutter Plugin,层出不穷的第三方库让你感到头皮发凉。所以我不过多介绍大家都应该懂了吧,Dart 是最好的语言~

Engine 层

Flutter 引擎,官方是这样介绍它的。

Flutter 的核心,主要使用 C++ 编写,提供了 Flutter 应用所需的原语。当需要绘制新一帧的内容时,引擎将负责对需要合成的场景进行栅格化。它提供了 Flutter 核心 API 的底层实现,包括图形(通过 Skia)、文本布局、文件及网络 IO、辅助功能支持、插件架构和 Dart 运行环境及编译环境的工具链。

我觉得官方描述得已经很详细了,总结一下就是:它负责 Flutter UI 的渲染以及宿主的交互,接入了 Engine 的就叫做宿主,如 Android 平台接入了 Flutter Engine,那么宿主就是 Android 平台。

Flutter Engine 是开源的,在 github 上可以找到,传送门 https://github.com/flutter/engine 直观感受下这个 Engine 项目都是用哪些语言编写的,如下图。其实主要语言还是 C++,其中 Dart 很多是测试代码。

Flutter Engine
Flutter Engine

Embedder 平台嵌入层

官方是这样介绍它的。

平台嵌入层是用于呈现所有 Flutter 内容的原生系统应用,它充当着宿主操作系统和 Flutter 之间的粘合剂的角色。当你启动一个 Flutter 应用时,嵌入层会提供一个入口,初始化 Flutter 引擎,获取 UI 和栅格化线程,创建 Flutter 可以写入的纹理。嵌入层同时负责管理应用的生命周期,包括输入的操作(例如鼠标、键盘和触控)、窗口大小的变化、线程管理和平台消息的传递。 Flutter 拥有 Android、iOS、Windows、macOS 和 Linux 的平台嵌入层,当然,开发者可以创建自定义的嵌入层

我觉得官方这段解释得同样很好,我几乎无法有更好的解释~但我还是要谈谈自己的见解,因为从程序员的角度看代码比谈概念更具体。Embedder 层的代码同样包含于 Engine 项目中,如下图。

Flutter Embedder
Flutter Embedder

Embedder 层代码整体可以分为两大块

  • 各平台的 Embedder 层代码,用于接入 Flutter Engine。如 Android 平台 Embedder 是用 Java 写的,当然还有与其对应的 C++ 代码,方便 Java 调用 JNI 来和 Flutter Engine 之间通信。自然 IOS 就是 Object-C 了。
  • 共用的 Embedder 层代码,C++ 编写, 用于平台消息传递等功能。

了解了 Flutter 架构,下面开始进入实战环节。

Flutter 如何与特定平台进行通信

本小节再次强调“特定平台”这个概念,因为 Flutter 它只是一个 UI 跨平台的框架,读者需要牢记于心,凡是涉及到平台相关的功能,还是必须要由原生平台来实现。本文以 Flutter 在 Android 平台上的应用,来讲解 Flutter 该怎样和 Android 原生进行通信

Flutter 与特定平台进行通信的流程大致是这样的。

  • Flutter 通过类似 JNI 方法调用的方式与 Flutter Engine 通信
  • Flutter Engine 层调用特定平台的 Embedder 层代码
  • 特定平台接收到来自 Embedder 层的消息,进行业务处理

反之,特定平台想和 Flutter 进行通信,过程刚好和上面相反。

使用 MethodChannel 通道进行方法调用

Flutter 与特定平台进行通信需要通过“平台通道”,细心的读者可能已经发现,平台通道(Platform Channels)被官方划分在 Engine 层。平台通道分为三种类型,分别是 BasicMessageChannel、MethodChannel、EventChannel。其中 MethodChannel 它使用异步方法调用的方式进行平台通信,这也是最常用的一种方式。

编写 Flutter 端代码

这里以官方的一个手机电量查询例子来演示整个方法调用过程,Flutter 端代码如下。优雅的做法是,遵循职责单一的原则,将每一个方法通道封装成一个类。比如我这个通道是用来管理电量的,那么就叫 BatteryChannel,所有和电量有关的方法都封装在这个类中。

class BatteryChannel {
  static const _batteryChannelName = "cn.blogss/battery";  // 1.方法通道名称
  static MethodChannel _batteryChannel;

  static void initChannels(){
    _batteryChannel = MethodChannel(_batteryChannelName);  // 2. 实例化一个方法通道
  }

  // 3. 异步任务,通过平台通道与特定平台进行通信,获取电量,这里的宿主平台是 Android
  static getBatteryLevel() async {  
    String batteryLevel;
    try {
      final int result = await _batteryChannel.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    return batteryLevel;
  }
}

在你需要使用这个通道的时候,在 Flutter 页面中这样做就行了。

class BatteryRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return BatteryRouteState();
  }
}

class BatteryRouteState extends State<BatteryRoute> {
  String _batteryLevel = 'Unknown battery level.';
  // 3.异步获取到电量,然后重新渲染页面
  getBatteryLevel() async{
    _batteryLevel = await BatteryChannel.getBatteryLevel();
    setState(() {});
  }

  @override
  void initState() {
    super.initState();
    BatteryChannel.initChannels();  // 1. 初始化通道
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("BatteryRoute"),
        centerTitle: true,
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            new ElevatedButton(
              child: new Text('Get Battery Level'),
              onPressed: (){
                getBatteryLevel();  // 2. 调用通道方法
              },
            ),
            new Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}

编写 Android 端代码

Android 端代码与 Flutter 端类似,如下所示。同样这个类叫 BatteryChannel,下面是 Kotlin 写的,没了解过的读者理解上来可能有点难度。不过它也很简单,这个通道就负责电量的查询了。只需实现 MethodChannel.MethodCallHandler 接口,重写 onMethodCall 方法,这样 Flutter 端的方法调用就会进入到这里。

class BatteryChannel(flutterEngine: BinaryMessenger, context: Context): MethodChannel.MethodCallHandler {
    private val batteryChannelName = "cn.blogss/battery"
    private var channel: MethodChannel
    private var mContext: Context

    companion object {
        private const val TAG = "BatteryChannel"
    }

    init {
        Log.d(TAG, "init")
        channel = MethodChannel(flutterEngine, batteryChannelName)
        channel.setMethodCallHandler(this)
        mContext = context;
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        Log.d(TAG, "onMethodCall: " + call.method)
        if (call.method == "getBatteryLevel") {
            val batteryLevel = getBatteryLevel()
            if (batteryLevel != -1) {
                result.success(batteryLevel)
            } else {
                result.error("UNAVAILABLE""Battery level not available.", null)
            }
        } else {
            result.notImplemented()
        }
    }

    private fun getBatteryLevel(): Int {
        val batteryLevel: Int
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val batteryManager = mContext.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(mContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) *
                    100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }

        return batteryLevel
    }
}

然后我们还要在 MainActivity 中实例化一下这个通道,如下。常用的做法是在 configureFlutterEngine 这个方法中实例化我们的通道就行了,有多少个通道,就在这里实例化多少个通道。

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        BatteryChannel(flutterEngine.dartExecutor.binaryMessenger,context)  // 实例化通道
    }
}

最后看下效果。 Battery Level

写在最后

本文详细介绍了 Flutter 框架概览,分析了 Dart Framework、Engine、Embedded 三层实际开发中所处的位置,然后通过代码实战,Flutter 通过平台通道与 Android 平台进行通信,查询到了手机电量。读者应该对这两部分知识有了深刻的理解与运用,下一篇,我将继续带领大家更深层次的探索平台通道通信机制!

如果你对我感兴趣,请移步到 http://blogss.cn ,进一步了解。

  • 如果本文帮助到了你,欢迎点赞和关注 ❤️
  • 由于作者水平有限,文中如果有错误,欢迎在评论区指正 ✔️

ThatcherLi

2021/05/06  阅读:153  主题:默认主题

作者介绍

ThatcherLi