oh, 我亲爱的朋友,很高兴你来到了这里!既然来了,那么就让我们在这篇糟糕的烂文章中,一起来学习一下,如何在一个糟糕的 Flutter 混合应用中开发一个糟糕的 Android Native 烂插件吧!😑
首先,先考虑第一个问题:混合开发中如何将Flutter集成到现有的Android应用中呢?
主要步骤:
- 首先,创建Flutter module;
- 为已存在的Android应用添加Flutter module依赖;
- 在Koltin中调用Flutter module;
- 编写Dart代码;
- 运行项目;
- 热重启/重新加载;
- 调试Dart代码;
- 发布应用;
在 Android Studio 中创建 Flutter module
在做混合开发之前我们首先需要创建一个Flutter module。
假如你的Native项目是这样的:xxx/flutter_hybrid/Native项目
$ cd xxx/flutter_hybrid/
// 创建 flutter_module
$ flutter create -t module flutter_module
// 如果需要指定包名
$ flutter create -t module --org com.example.xxx flutter_module
上面代码会切换到你的 Android/iOS
项目的上一级目录,并创建一个 flutter_module
模块。
打开 flutter_module
,查看其中的文件结构,你会发现它里面包含.android
与.ios
,这两个文件夹是隐藏文件,也是这个 flutter_module
宿主工程:
.android
:flutter_module 的Android宿主工程;.ios
:flutter_module 的iOS宿主工程;lib
:flutter_module 的Dart部分的代码;pubspec.yaml
:flutter_module 的项目依赖配置文件;
因为宿主工程的存在,我们这个
flutter_module
在不加额外的配置的情况下是可以独立运行的,通过安装了Flutter与Dart插件的Android Studio打开这个flutter_module
项目,通过运行按钮是可以直接运 行它的。
为已存在的 Android 应用添加 Flutter module 依赖
接下来就需要将创建的Flutter module依赖到我们Android的主工程,有如下两种方式可以依赖
方式一:构建 flutter aar(非必须)
如果你需要的话,可以通过如下命令来构建 flutter aar:
$ cd .android/
$ ./gradlew flutter:assembleRelease
这会在 .android/Flutter/build/outputs/aar/
中生成一个 flutter-release.aar
归档文件。
使用这种方式的好处是我们可以把自己生成的flutter aar上传到自己公司的Maven仓库中给别人使用,这样开发Flutter的人和开发Android原生代码的人就可以分开独立工作,各干各的,不用在同一个工程里面折腾。(但是假如你的公司中的app开发只有你一个人的话,那我只能 deeply sorry for that)
方式二:在settings.gradle添加依赖
打开我们Android项目的 settings.gradle
添如下代码:
include ':app' // 已存在
//for flutter
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'flutter_module/.android/include_flutter.groovy' // new
))
//可选,主要作用是可以在当前AS的Project下显示flutter_module以方便查看和编写Dart代码
include ':flutter_module'
project(':flutter_module').projectDir = new File('../flutter_module')
setBinding
与 evaluate
允许Flutter模块包括它自己在内的任何Flutter插件,在 settings.gradle
中以类似::flutter
、:video_player
的方式存在。
此时再同步一下项目。
宿主模块添加 :Flutter 依赖
在app module下的build.gradle
中添加:
dependencies {
implementation project(':flutter')
...
}
如果工程中很多地方都需要用到它,可以将其放到common module中添加。
添加 Java 8 编译选项
因为Flutter的Android engine使用了Java 8 的特性,所以在引入Flutter时需要配置你的项目的 Java 8 编译选 项:
在你的app的 build.gradle
文件的 android {}
节点下添加:
android {
// ...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
配置CPU架构
在app module下的build.gradle
中添加:
android {
// ...
defaultConfig {
// 配置Flutter支持的架构
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86_64'
}
}
}
注意:
- 从
Flutter v1.17
版本开始,Flutter module仅仅支持AndroidX
的应用。- 在
release
模式下Flutter仅支持以下架构:x86_64,armeabi-v7a,arm64-v8a
,不支持mips和x86;所以引入Flutter前需要选取Flutter支持的架构。
在 Koltin 中调用 Flutter module
至此,我们已经为我们的Android项目添加了Flutter所必须的依赖,接下来我们来看如何在项目上以Kotlin的方式在Fragment中调用Flutter模块。
在 Android 中调用 Flutter 模块的有两种方式:
- 1.使用 Flutter.createView API 方式创建 (作为页面的一部分)Flutter.createView() 已经被官方弃用 Flutter 1.12版本废弃了io.flutter.facade包导致的
- 2.使用 FlutterFragment.createDefault() 来创建
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.test).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//没有指定路由 传值;只能调到 默认路由(开始界面)
// FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
// fragmentTransaction.add(R.id.someContainer, FlutterFragment.createDefault());
// //fragmentTransaction.replace(R.id.someContainer, FlutterFragment.createDefault());
// fragmentTransaction.commit();
//指定路由并且传值
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.initialRoute("{name:'devio',dataList:['aa','bb','bb']}")
.build();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.someContainer, flutterFragment)
.commit();
}
});
}
}
原生传值可以直接接收到参数:
import 'dart:ui' as ui;
final String initParams = ui.window.defaultRouteName;
编辑的时候:
- 编辑原生原生代码只能修改原生的代码;
- 编辑dart 代码: Flutter_module lib 文件夹里面的代码;
直接运行 Flutter_module:只能运行 Flutter_module里面的工程;
也可以不用Flutter系统为我们准备的FlutterFragment
,自己新建一个Fragment处理:
abstract class HiFlutterFragment : HiBaseFragment() {
protected lateinit var flutterEngine: FlutterEngine
override fun onAttach(context: Context) {
super.onAttach(context)
flutterEngine = FlutterEngine(context)
//让引擎执行Dart代码
flutterEngine.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())
}
}
在Fragment的布局中添加Flutter View:
其中有一点需要稍加解释一下,就是图中注释的部分,渲染FlutterView的两种方式,除了FlutterTextureView
之外,还有一个FlutterSurfaceView
:
但是这里不采用它的原因是由于当app切后台FlutterSurfaceView
是会有复用的问题,比如:
此时将app切到后台,然后再切到前台时,推荐页面的FlutterView会被复用到收藏页面的FlutterView上的,很显然是不对的,所以这一点需要明确。
为了能让Flutter感知到自己创建的Fragment的各个生命周期,所以需要重写一系列的生命周期方法,如下:
接下来在宿主项目中使用一下上面自定义的可以承载Flutter的Fragment,在宿主中找一个页面替换成Flutter view,例如将首页的推荐页面替换成Flutter页面:
其中有个title文本资源:<string name="title_recommend">精选推荐</string>
一切就绪后,运行看一下效果:
等于就是把官方的demo给嵌入到了咱们的推荐页面上了,至此,混编的第一步已经搭建好了。
热重启 /重启加载
在混合开发中 Android 项目集成 Flutter 项目的时候,如果发现重启/重新加载不起作用了,那在混合开发中怎么启用重启/重新加载呢:
- 手机连接我们的 电脑
- 关闭我们的App应用;然后运行
flutter attach
; (在对应的 flutter_module 项目根路径)
注意:如果你同时有多个模拟器或连接的设备,运行flutter attach
会提示你选择一个设备,接下来我们需要flutter attach -d 设备ID
来指定一个设备:如flutter attach -d emulator-5554
- 当出现 “Waiting for a connection from Flutter on PACM00…” 的时候打开我们原生App;并且进入我们的 Flutter 界面
然后会提示同步信息和 命令信息
D:\MineGit\flutter_trip\flutter_module_john>flutter attach
Multiple devices found:
SM G9650 (mobile) • 21a9f15c1d037ece • android-arm64 • Android 10 (API 29)
PACM00 (mobile) • JZU8PB9DQOG68D6D • android-arm64 • Android 10 (API 29)
[1]: SM G9650 (21a9f15c1d037ece)
[2]: PACM00 (JZU8PB9DQOG68D6D)
Please choose one (To quit, press "q/Q"): 2
Waiting for a connection from Flutter on PACM00...
Syncing files to device PACM00... 7.4s
Flutter run key commands.
r Hot reload.
R Hot restart.
h Repeat this help message.
d Detach (terminate "flutter run" but leave application running).
c Clear the screen
q Quit (terminate the application on the device).
Running with sound null safety
现在你只要 修改完毕 dart 代码保存;然后在 按 r 键就能立马看到效果了。
调试 dart 代码
在混合开发模式下, 如何更好的调试我们的代码:
- 关闭App(这一步很关键)
- 点击 Android Studio 的 Flutter Attch 按钮(前提是 安装过flutter 与 dart 插件)
- 打上断点,启动App,就能进入对应的断点 了
接下来就可以像调试普通Flutter项目一样来调试混合开发模式下的Dart代码了。
除了以上步骤不同之外,还有一点需要注意:在运行Android工程时,一定要在Android模式下的AndroidStuio中运行,因为Flutter模式下的AndroidStudio运行的是Flutter module下的.android中的Android工程。
复杂场景下的Flutter混合架构设计
通常Flutter混合设计是这样的形态:
也就是将要打开的某一个页面的整个页面使用Flutter View来实现。而复杂场景就是像下面这种:
也就是一个页面中既有原生View 又有 Flutter View。
为啥复杂呢?这是因为Flutter可以理解是一个单页面应用, 所以并不支持像这种一个页面中既有native又有flutter的场景。
优化:秒开Flutter模块
目前我们初步在推荐模块中集成的Flutter运行起来会比较慢,因为我们目前是在Fragment中每次都来初始化Flutter引擎,如下:
要实现秒开的效果,则需要使用预加载,但是预加载很显然会影响到首页加载的性能,所以如何让预加载不损失"首页"性能成了我们需要解决的问题,下面一个个来。
1、预加载逻辑实现:
新建一个单例类HiFlutterCacheManager
,并在其中初始化FlutterEngine
:
import android.content.Context
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.view.FlutterMain
/**
* Flutter优化提升加载速度,实现秒开Flutter模块
* 0.如何让预加载不损失"首页"性能
* 1.如何实例化多个Flutter引擎并分别加载不同的dart 入口文件
*/
class HiFlutterCacheManager private constructor() {
// 初始化FlutterEngine
private fun initFlutterEngine(
context: Context,
moduleName: String
): FlutterEngine {
// Instantiate a FlutterEngine.
val flutterEngine = FlutterEngine(context)
// Start executing Dart code to pre-warm the FlutterEngine.
flutterEngine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint(
FlutterMain.findAppBundlePath(),
moduleName
)
)
// Cache the FlutterEngine to be used by FlutterActivity or FlutterFragment.
FlutterEngineCache.getInstance().put(moduleName, flutterEngine)
return flutterEngine
}
companion object {
@JvmStatic
@get:Synchronized
var instance: HiFlutterCacheManager? = null
get() {
if (field == null) {
field = HiFlutterCacheManager()
}
return field
}
private set
}
}
其中可以看到,缓存的key
是moduleName
,刚好供之后具体模块的使用。
【预加载FlutterEngine
的核心逻辑】:
接下来就来提供一个预加载的方法,其中有一个小技巧值得学习:
/**
* 预加载FlutterEngine
*/
fun preLoad(
context: Context
) {
//在线程空闲时执行预加载任务,这样就不会和主线程进行争抢了,只有在主线程空闲时才会执行预加载
Looper.myQueue().addIdleHandler {
initFlutterEngine(context, MODULE_NAME_FAVORITE)
initFlutterEngine(context, MODULE_NAME_RECOMMEND)
false
}
}
获取预加载的FlutterEngine
:
/**
* 获取预加载的FlutterEngine
*/
fun getCachedFlutterEngine(moduleName: String, context: Context?): FlutterEngine? {
var engine = FlutterEngineCache.getInstance()[moduleName]
if (engine == null && context != null) {
engine = initFlutterEngine(context, moduleName)
}
return engine!!
}
2、调用HiFlutterCacheManager开启预加载:
在宿主的Application
当中调用HiFlutterCacheManager
的preLoad
方法预加载Flutter的Engine
:
import android.app.Application
import com.google.gson.Gson
import org.devio.`as`.proj.common.flutter.HiFlutterCacheManager
import org.devio.hi.library.log.HiConsolePrinter
import org.devio.hi.library.log.HiFilePrinter
import org.devio.hi.library.log.HiLogConfig
import org.devio.hi.library.log.HiLogConfig.JsonParser
import org.devio.hi.library.log.HiLogManager
open class HiBaseApplication : Application() {
override fun onCreate() {
super.onCreate()
initLog()
//Flutter 引擎预加载,让其Flutter页面可以秒开
HiFlutterCacheManager.instance?.preLoad(this)
}
private fun initLog() {
HiLogManager.init(object : HiLogConfig() {
override fun injectJsonParser(): JsonParser {
return JsonParser {
src: Any? ->
Gson().toJson(src)
}
}
override fun includeThread(): Boolean {
return true
}
}, HiConsolePrinter(), HiFilePrinter.getInstance(cacheDir.absolutePath, 0))
}
}
3、在HiFlutterFragment中使用HiFlutterCacheManager:
4、修改RecommendFragment:
由于基类调整了,子类相应也得进行修改,如下:
5、改造FavoriteFragment:
为了看到效果,我们对收藏页面也进行相应的代码集成:
这样对于native的代码就已经改造完了,接下来则则可以来修改Flutter代码了。
6、修改flutter代码:
先找到flutter_module,对于flutter代码的编写可以切到project视图,找到它:
注意,它的出现,前提是一定要在这里进行配置这句话:
这样就省得在Android和Flutter之间的环境进行切换了,全在一个工程中都可以搞定了,还是非常有用的技巧。
在 flutter_module/lib
下面新建page
目录,在其中新建收藏和推荐两个页面的dart
文件:
修改main.dart
【重点】:
如何实例化多个Flutter引擎并分别加载不同的dart 入口文件呢?此时就需要回到main.dart文件中来添加支持了,原本Flutter只支持一个main.dart
入口的, 此时咱们要加载多个模块的dart入口,怎么办呢?此时就需要进行改造了:此时将MyApp
改为home
参数是可以动态更改的。
而我们在宿主中注册Flutter引擎时会提供指定的页面名称:
这里默认的值main
字符串就对应了main.dart
中的main()
方法,因此接下来就需要在main.dart
中再创建一个推荐页面的入口了,依葫芦画瓢:
但是!!!这样只是创建了一个recommend入口Flutter是不会加载它的, 需要向Flutter注册一下,具体方法如下:
主要是通过 @pragma('vm:entry-point')
这个注解指定多个入口。
Flutter与Native的通信机制
Flutter和Native的通信是通过Channel来完成的。
消息使用Channel(平台通道)在客户端(UI)和主机(平台)之间传递,如下图所示:
-
应用中的 Flutter 部分通过Platform Channel向其宿主 (非 Dart 部分) 发送消息。
-
宿主监听Platform Channel并接收消息。然后,它使用原生编程语言来调用任意数量的相关平台 API,并将响应发送回 Flutter 。
消息和响应以异步的形式进行传递,以确保用户界面能够保持响应。
Flutter 是通过 Dart 异步发送消息的。即便如此,当你调用一个平台方法时,也需要在主线程上做调用。
Flutter端在调用方法的时候 MethodChannel 会负责响应,从平台一侧来讲,Android 系统上使用 MethodChannelAndroid
、 iOS 系统使用 MethodChanneliOS
来接收和返回来自 MethodChannel
的方法调用。在开发平台插件的时候,可以减少样板代码。
Platform Channel支持的数据类型
标准平台通道使用标准消息编解码器,它支持简单的类似 JSON 值的高效二进制序列化,例如布尔值、数字、字符串、字节缓冲区及这些类型的列表和映射(详情请参阅 StandardMessageCodec)。当你发送和接收值时,它会自动对这些值进行序列化和反序列化。
以下是Platform Channel支持的kotlin
和Dart
的对应数据类型:
其他语言请参考 table from the Flutter documentation
Platform Channel 的三种分类
Flutter定义了三种不同类型的Channel来与原生通信:
Channel 类型 | 用途 | 交互方向 | 一个示例 |
---|---|---|---|
BasicMessageChannel | 低级消息,传递字符串和半结构化信息 | 双向 | 自定义编解码器 |
MethodChannel | 请求-响应(类似 RPC)类型的方法调用,传递方法调用 | 双向 | 调用本地代码 |
EventChannel | 事件驱动流,传递数据流信息 | 双向 | 订阅原生事件 |
这三种方式,无论是传递方法还是事件,本质上都是传递的数据。
具体使用哪种通信方式,要看使用场景,同一种功能可以通过不同的方式来实现。比如获取手机电量,网络变化等可以通过 EventChannel
,也能用 MethodChannel
。比如Flutter调用Native拍照可以用MethodChannel
。 如果要通过Flutter控制一个原生的视频播放组件,则最好通过MethodChannel
;如果要视频播放期间获取播放进度改变、当前网速变化等信息,则最好通过EventChannel
。
Flutter 与 Android 原生通信示例
以 Flutter 获取显示 Android 手机的电池电量为例,使用 MethodChannel
来实现。
1、Flutter 端的代码配置
设置 Flutter 端的通道比较简单,一共需要两步
- 第一步:生成一个 Flutter 和 Android 原生通信的通道
MethodChannel
; - 第二步:通过
MethodChannel
对象发起一次方法的调用invokeMethod
。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class _MyHomePageState extends State<MyHomePage> {
static const platform = MethodChannel('samples.flutter.dev/battery');
// Get battery level.
MethodChannel
构造时需要传递一个name
名称,一个应用中所使用的所有通道名称必须是唯一的;因此官方建议为通道名称添加唯一的前缀,比如:samples.flutter.dev/battery
。
接下来,在MethodChannel
上通过invokeMethod
调用方法(通过指定 String
类型的 getBatteryLevel
字符串调用具体方法)。调用可能会失败,比如,如果平台不支持此平台 API(比如在模拟器中运行),所以需要将 invokeMethod
调用包裹在 try-catch
语句中。
// Get battery level.
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${
e.message}'.";
}
// 调用 setState 使用返回的 batteryLevel 来更新 _batteryLevel 的界面状态。
setState(() {
_batteryLevel = batteryLevel;
});
}
最后,将模板中的 build 方法替换为包含以字符串形式显示电池状态、并包含一个用于刷新该值的按钮的小型用户界面。
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _getBatteryLevel, // 点击按钮时调用上面的方法请求Android端
child: const Text('Get Battery Level'),
),
Text(_batteryLevel), // 显示电池状态
],
),
),
);
}
2、Android 端的代码配置
找到 MainActivity.kt
文件,在 MainActivity
中添加以下代码:
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity() {
private val CHANNEL = "samples.flutter.dev/battery" // 确保与 Flutter 客户端使用的一致
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
// This method is invoked on the main thread.
// TODO
}
}
}
这里主要是在 configureFlutterEngine()
方法中创建一个 MethodChannel
并调用 setMethodCallHandler()
。确保使用的Channel名称与 Flutter 客户端使用的一致。
当使用特定的 Android Activity 实例初始化 Flutter Engine时会调用configureFlutterEngine
方法,因此 Flutter 推荐使用它来注册方法通道处理程序。
接下来添加使用 Android battery API 来检索电池电量的 Android Kotlin 代码。该代码与你在原生 Android 应用中编写的代码完全相同。
首先在文件头部添加所需的依赖:
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
然后在 MainActivity
类中的 configureFlutterEngine()
方法下方添加以下新方法:
private fun getBatteryLevel(): Int {
val batteryLevel: Int
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
} else {
val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
}
return batteryLevel
}
最后,完成前面添加的 onMethodCall()
方法。你需要处理单个平台方法 getBatteryLevel()
,所以在参数 call
中对其进行验证。该平台方法的实现是调用上一步编写的 Android 代码,并使用 result
参数来返回成功和错误情况下的响应。如果调用了未知方法,则报告该方法。
删除以下代码:
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
// This method is invoked on the main thread.
// TODO
}
并替换成以下内容:
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
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()
}
}
这里通过call.method
判断Dart发来的方法名称,你可以写多个分支,处理多个来自Dart的方法调用。
注意,这里处理未知的方法名最好方式是返回 result.notImplemented()
:
// ---
else {
result.notImplemented()
}
现在你应该可以在 Android 中运行该应用。如果使用了 Android 模拟器,请在扩展控件面板中设置电池电量,可从工具栏中的 … 按钮访问。
将数据从 Kotlin 返回到 Dart
通过前面的示例,你已经知道该如何做:result.success(arg)
,开发者无需做其他工作,只要传递的参数类型是Platform Channel所支持的数据类型(参考前文)。
将参数从 Dart 传递到 Kotlin
类似的,在 Dart 中MethodChannel
对象的invokeMethod
方法也可以传递额外的参数给平台。
以下是一个简单示例,通过MethodChannel
向Android端调用方法请求返回一个随机字符串:
// 该方法向Native平台调用getRandomString请求返回一个随机数字符串
Future<void> _generateRandomString() async {
String random = '';
try {
var arguments = {
'len': 3, // 随机字符串长度
'prefix': 'fl_', // 随机字符串的前缀
};
random = await platform.invokeMethod('getRandomString', arguments);
} on PlatformException catch (e) {
random = '';
}
setState(() {
_counter = random;
});
}
在 Kotlin 代码中获取Dart传来的参数:
if(call.method == "getRandomString") {
val limit = call.argument("len") ?: 4
val prefix = call.argument("prefix") ?: ""
val rand = ('a'..'z')
.shuffled()
.take(limit)
.joinToString(prefix = prefix, separator = "")
result.success(rand)
}
这里使用的主要是 call.argument("argName")
来获取参数值,注意可能获取失败,会返回null
, 因此提供了默认值。
如果你只需要传递一个方法参数,那么可以直接传,而不需要像上面代码那样搞一个动态Map:
// Dart:
random = await platform.invokeMethod('getRandomString', 3);
// Kotlin
val limit = call.arguments() ?: 4
val rand = ('a'..'z')
.shuffled()
.take(limit)
.joinToString("")
result.success(rand)
通过 Pigeon 获得类型安全的通道
在之前的样例中,我们使用 MethodChannel 在 host 和 client 之间进行通信,然而这并不是类型安全的。为了正确通信,调用/接收消息取决于 host 和 client 声明相同的参数和数据类型。 Pigeon 包可以用作 MethodChannel 的替代品,它将生成以结构化类型安全方式发送消息的代码。
在 Pigeon 中,消息接口在 Dart 中进行定义,然后它将生成对应的 Android 以及 iOS 的代码。更复杂的例子以及更多信息请查看 pigeon。
使用 Pigeon 消除了在主机和客户端之间匹配字符串的需要消息的名称和数据类型。它支持:嵌套类,消息转换为 API,生成异步包装代码并发送消息。生成的代码具有相当的可读性并保证在不同版本的多个客户端之间没有冲突。支持 Objective-C,Java,Kotlin 和 Swift(通过 Objective-C 互操作)语言。
Pigeon 样例:
import 'package:pigeon/pigeon.dart';
class SearchRequest {
final String query;
SearchRequest({
required this.query});
}
class SearchReply {
final String result;
SearchReply({
required this.result});
}
()
abstract class Api {
SearchReply search(SearchRequest request);
}
Dart 用法:
import 'generated_pigeon.dart';
Future<void> onClick() async {
SearchRequest request = SearchRequest(query: 'test');
Api api = SomeApi();
SearchReply reply = await api.search(request);
print('reply: ${
reply.result}');
}
错误处理策略
编程中有两种主要的错误处理策略:基于错误代码和基于异常。一些程序员通过混合使用两者来使用混合错误处理策略。
MethodChannel
内置支持为 Kotlin 端的错误流处理来自 Dart 的异常。此外,它还提供了一种方法来区分本机错误类型和异常实例中的错误代码。换句话说,MethodChannel
为 Flutter 开发人员提供了一种混合错误处理策略。
在前面的示例中,我们使用result.success
方法返回一个值并处理未知的方法调用,这将在 Dart 中抛出result.notImplementedMissingPluginException
。
如果我们需要从 Kotlin 端创建 Dart 异常怎么办?result.error
方法可帮助您从 Kotlin 中抛出一个 Dart 的 PlatformException
实例。假设如果我们在前面请求随机字符串的示例中,当请求的随机字符串长度为负值时,需要抛出异常,那么可以这样修改:
// kotlin
if(call.method == "getRandomString") {
val limit = call.arguments() ?: 4
if(limit < 0) {
result.error("INVALIDARGS", "String length should not be a negative integer", null)
}
else {
val rand = ('a'..'z')
.shuffled()
.take(limit)
.joinToString("")
result.success(rand)
}
}
接下来,在 Dart 端捕获异常并使用它,如下所示:
Future<void> _generateRandomString() async {
String random = '';
try {
random = await platform.invokeMethod('getRandomString', -5);
} on PlatformException catch (e) {
random = '';
print('PlatformException: ${
e.code} ${
e.message}');
}
setState(() {
_counter = random;
});
}
当您运行应用程序并按下操作按钮时,您将在终端上看到异常代码和消息,因为我们从 Dart 端传递了 -5
作为字符串长度:
正如我们在上面的示例中看到的,您可以在 Dart 中进行捕获PlatformException,并且可以在异常实例中查看错误代码以处理方法通道错误。另一种更抽象的方法是根据 Kotlin 错误代码创建您自己的异常实例。请参考 Flutter 的 camera plugin 中的 CameraException 实现。
使用 EventChannel
MethodChannel
与传统的 RESTful API 一样,该类提供基于请求-响应的通信解决方案。如果我们在使用 Web 应用程序时需要从服务器调用客户端怎么办?那么,我们倾向于选择像WebSockets这样的事件驱动的通信机制。EventChannel
类提供了一个异步事件流,用于在本机主机应用程序和 Flutter 之间构建事件驱动的通信线路。EventChannel
类主要用于将本机事件发送到 Dart 端。
例如,我们可以将系统主题更改事件从 Kotlin 调度到 Dart。此外,我们可以用EventChannel
来广播来自设备传感器的频繁事件。
下面是示例中,我们将通过一个EventChannel
实例来自动检测当前的系统颜色主题。
首先,将以下代码添加到MainActivity.kt
中:
package com.example.flutter_platform_channels_demo
import kotlin.random.Random
import androidx.annotation.NonNull
import android.os.Bundle
import android.content.res.Configuration
import android.content.pm.ActivityInfo
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.EventChannel.EventSink
import io.flutter.plugin.common.EventChannel.StreamHandler
class MainActivity: FlutterActivity() {
var events: EventSink? = null
var oldConfig: Configuration? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
oldConfig = Configuration(getContext().getResources().getConfiguration())
}
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
EventChannel(flutterEngine.dartExecutor.binaryMessenger, "example.com/channel").setStreamHandler(
object: StreamHandler {
override fun onListen(arguments: Any?, es: EventSink) {
events = es
events?.success(isDarkMode(oldConfig))
}
override fun onCancel(arguments: Any?) {
}
}
);
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if(isDarkModeConfigUpdated(newConfig)) {
events?.success(isDarkMode(newConfig))
}
oldConfig = Configuration(newConfig)
}
fun isDarkModeConfigUpdated(config: Configuration): Boolean {
return (config.diff(oldConfig) and ActivityInfo.CONFIG_UI_MODE) != 0
&& isDarkMode(config) != isDarkMode(oldConfig);
}
fun isDarkMode(config: Configuration?): Boolean {
return config!!.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}
}
我们使用EventChannel
类来创建事件驱动的通信流。一旦EventChannel Handler
附加之后,我们就可以使用onListen
方法中回传的EventSink
实例将事件发送到 Dart 端。在以下情况下会发生事件:
- 当 Flutter 应用初始化时,事件通道将收到一个具有当前主题状态的新事件
- 当用户从系统设置页面更改系统主题后返回应用时,事件通道将收到一个具有当前主题状态的新事件
请注意,这里我们使用一个boolean
值作为事件参数来标识暗模式是否被激活。
现在,将以下代码添加到main.dart
文件:
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({
super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(),
darkTheme: ThemeData.dark(),
themeMode: ThemeMode.system,
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({
super.key, required this.title});
final String title;
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
String _theme = '';
static const events = EventChannel('example.com/channel')