Flutter 的优势与缺点
Flutter 作为一个跨平台的移动 UI 框架,可以快速在 ios 和 android 上构建高质量的原生用户界面。可以说是一套代码做到了多端运行,我们常说的 Flutter 跨平台,其实是 UI 跨平台,编写好了一套 UI 代码,就可以在 IOS、Android、Web 上呈现出同样的效果,节省了大量的人力成本,这也是 Flutter 越来越流行的原因之一。
然而 Flutter 也有它的缺点,很明显的是它并不能直接调用平台的系统功能,比如使用蓝牙、相机、GPS、音量、电池等,因此要在 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 上可以找到,传送门 github.com/flutter/eng… 直观感受下这个 Engine 项目都是用哪些语言编写的,如下图。其实主要语言还是 C++,其中 Dart 很多是测试代码。
Embedder 平台嵌入层
官方是这样介绍它的。
平台嵌入层是用于呈现所有 Flutter 内容的原生系统应用,它充当着宿主操作系统和 Flutter 之间的粘合剂的角色。当你启动一个 Flutter 应用时,嵌入层会提供一个入口,初始化 Flutter 引擎,获取 UI 和栅格化线程,创建 Flutter 可以写入的纹理。嵌入层同时负责管理应用的生命周期,包括输入的操作(例如鼠标、键盘和触控)、窗口大小的变化、线程管理和平台消息的传递。 Flutter 拥有 Android、iOS、Windows、macOS 和 Linux 的平台嵌入层,当然,开发者可以创建自定义的嵌入层
我觉得官方这段解释得同样很好,我几乎无法有更好的解释~但我还是要谈谈自己的见解,因为从程序员的角度看代码比谈概念更具体。Embedder 层的代码同样包含于 Engine 项目中,如下图。
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 createState() {
return BatteryRouteState();
}
}
class BatteryRouteState extends State {
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) // 实例化通道
}
}
最后看下效果。
写在最后
本文详细介绍了 Flutter 框架概览,分析了 Dart Framework、Engine、Embedded 三层实际开发中所处的位置,然后通过代码实战,Flutter 通过平台通道与 Android 平台进行通信,查询到了手机电量。读者应该对这两部分知识有了深刻的理解与运用,下一篇,我将继续带领大家更深层次的探索平台通道通信机制!