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

Flutter 的优势与缺点

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

然而 Flutter 也有它的缺点,很明显的是它并不能直接调用平台的系统功能,比如使用蓝牙、相机、GPS、音量、电池等,因此要在 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

Embedder 平台嵌入层

官方是这样介绍它的。

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

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

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 ,进一步了解。

  • 如果本文帮助到了你,欢迎点赞和关注 ❤️
  • 由于作者水平有限,文中如果有错误,欢迎在评论区指正 ✔️
  • 本文首发于简书,未经许可禁止转载 ©️
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容