序
Flutter混合开发中,一些基于原生开发的功能无法实现,需要编写原生Android和iOS的功能插件才能实现,这里就涉及到Flutter与原生代码之间的通信,本人也是刚从Android开发转向Flutter混合开发,所以编写此文记录一下Flutter与Android之间互相通信的学习,顺便记录一些错误以免以后再犯
新建项目
配置好Flutter环境以后,打开Android Studio,New -> New Flutter Project -> Flutter Application -> Next -> “flutter_plugin_demo” -> Next -> Finish
等待AS构建好Flutter项目后,修改android文件下的build.gradle文件:
因为Flutter默认配置的仓库依赖是国外的,要是在国内运行起来会很慢甚至运行失败,这里修改为阿里云的maven仓库依赖
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/jcenter' }
maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
连接虚拟机或手机,点击运行即可。
New -> New Moudle -> Flutter Plugin -> “flutter_plugin_test” -> Next -> Finish
flutter create --template=plugin flutter_plugin_test
//或者 其中 com.example为包名
flutter create --org com.example --template=plugin flutter_plugin_test
等待构建完成后,项目的目录结构如下(注:这里插件test文件夹标红不用管,不影响运行):
调用插件
调用插件之前,先修改一下主程序的main.dart文件,方便后期调用的操作
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// This widget is the root of your application.
String _msg = "这是内容";
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: "这是标题",
home: new Scaffold(
appBar: new AppBar(
title: new Center(
child: new Text("这是居中标题"),
)),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(_msg),
new MaterialButton(
onPressed: () {
_getPluginMsg();
},
child: new Text("获取插件信息"),
color: Colors.blue,
textColor: Colors.white,
)
],
)
)
),
);
}
_getPluginMsg() {
print("点击了按钮");
}
}
然后用Android Studio新窗口打开插件的android端项目,项目视图切换为Project,
接着从
flutter sdk安装路径\bin\cache\artifacts\engine
其中的任何一个文件夹找到flutter.jar,在刚才打开的android项目根目录下新建lib文件夹,并将flutter.jar拷贝到该目录下并右键点击Add As Library,导入之后 FlutterPluginTestPlugin.kt文件报错消失,我们就可以在该文件编写插件内容了。
首先分析下FlutterPluginTestPlugin.kt文件下的内容:
package com.example.flutter_plugin_test
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry.Registrar
class FlutterPluginTestPlugin: MethodCallHandler {
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
//初始化MethodChannel通信通道
//定义通道唯一名称 这个名称在flutter调用插件时要使用到 所以必须唯一 若有两个通道定义同一个名称 则后一个会覆盖前一个
val channel = MethodChannel(registrar.messenger(), "flutter_plugin_test")
channel.setMethodCallHandler(FlutterPluginTestPlugin())
}
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
//当flutter端调用方法getPlatformVersion时,返回当前系统的版本号:Android + 系统版本号
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
//没有实现flutter调用的方法时的回调
result.notImplemented()
}
}
}
flutter_plugin_test:
path: flutter_plugin_test
然后点击右上角 Packages get 获取刚才添加的插件
最后修改主程序main.dart内容调用插件方法:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// This widget is the root of your application.
String _msg = "这是内容";
//定义通信通道 名称与插件中的一致
static const platform = const MethodChannel("flutter_plugin_test");
//异步调用插件方法 获取返回内容
Future<void> _getMsg() async{
String msg ;
try{
msg = await platform.invokeMethod("getPlatformVersion");
}on PlatformException{
msg = "获取插件信息错误";
}
if(!mounted){
return;
}
setState(() {
_msg = "这是重插件获取的信息:"+msg;
});
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: "这是标题",
home: new Scaffold(
appBar: new AppBar(
title: new Center(
child: new Text("这是居中标题"),
)),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(_msg),
new MaterialButton(
onPressed: () {
_getPluginMsg();
},
child: new Text("获取插件信息"),
color: Colors.blue,
textColor: Colors.white,
)
],
)
)
),
);
}
_getPluginMsg() {
print("点击了按钮");
//点击按钮是 获取插件返回值
_getMsg();
}
}
最后运行程序,此时会报错:
D8: Program type already present: io.flutter.app.FlutterActivityDelegate$1
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:transformDexArchiveWithExternalLibsDexMergerForDebug'.
> com.android.builder.dexing.DexArchiveMergerException: Error while merging dex archives: D:\Android\workspace\flutter_plugin_demo\flutter_plugin_demo\build\app\intermediates\transforms\dexBuilder\debug\13.jar, D:\Android\workspace\flutter_plugin_demo\flutter_plugin_demo\build\app\intermediates\transforms\dexBuilder\debug\0.jar, D:\Android\workspace\flutter_plugin_demo\flutter_plugin_demo\build\app\intermediates\transforms\dexBuilder\debug\1.jar, D:\Android\workspace\flutter_plugin_demo\flutter_plugin_demo\build\app\intermediates\transforms\dexBuilder\debug\12.jar, D:\Android\workspace\flutter_plugin_demo\flutter_plugin_demo\build\app\intermediates\transforms\dexBuilder\debug\7.jar, D:\Android\workspace\flutter_plugin_demo\flutter_plugin_demo\build\app\intermediates\transforms\dexBuilder\debug\6.jar, D:\Android\workspace\flutter_plugin_demo\flutter_plugin_demo\build\app\intermediates\transforms\dexBuilder\debug\5.jar, D:\Android\workspace\flutter_plugin_demo\flutter_plugin_demo\build\app\intermediates\transforms\dexBuilder\debug\4.jar
Program type already present: io.flutter.app.FlutterActivityDelegate$1
Learn how to resolve the issue at https://developer.android.com/studio/build/dependencies#duplicate_classes.
这是因为刚才我们在插件的android项目中引入flutter.jar包,而主程序当中已经有了自己的jar包,才导致运行时两者冲突出错,只要将插件端android项目的flutter.jar包依赖注释掉再点击Sync Now,再重新运行一下主项目即可(注意:这里在android项目中引入flutter.jar是为了方便编写插件,实际导入插件或发布插件时记得把flutter.jar引入注释掉):
点击一下按钮:
OK,成功获取到插件中方法getPlatformVersion返回的内容,这里可以尝试修改一下插件FlutterPluginTestPlugin.kt里方法的返回值,然后重新运行程序,看看是否有改变
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
//修改返回值 也可以在这里进行其他操作 比如获取电量值等等
result.success("这是返回值")
} else {
//没有实现flutter调用的方法时的回调
result.notImplemented()
}
}
插件与Flutter通信
Flutter获取插件的内容成功了,那插件回传数据给Flutter(调用Flutter的方法,主动传值给Flutter)怎么办呢?
这里就要用到EventChannel了,MethodChannel和EventChannel具体原理大家可以到Flutter中文网更详细了解一下。
修改FlutterPluginTestPlugin.kt文件,加入EventChannel实现:
package com.example.flutter_plugin_test
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry.Registrar
/**
* 实现MethodCallHandler和EventChannel.StreamHandler
*/
class FlutterPluginTestPlugin: MethodCallHandler,EventChannel.StreamHandler {
companion object {
@JvmStatic
fun registerWith(registrar: Registrar) {
//初始化MethodChannel通信通道
//定义通道唯一名称 这个名称在flutter调用插件时要使用到 所以必须唯一 若有两个通道定义同一个名称 则后一个会覆盖前一个
val channel = MethodChannel(registrar.messenger(), "flutter_plugin_test")
channel.setMethodCallHandler(FlutterPluginTestPlugin())
//初始化EventChannel通信通道 定义唯一名称
val eventChannel = EventChannel(registrar.messenger(),"flutter_plugin_event")
eventChannel.setStreamHandler(FlutterPluginTestPlugin())
}
}
/**
* EventChannel通道接通 onListen方法
* 可以在此通过EventChannel.EventSink主动传值给Flutter
*/
override fun onListen(p0: Any?, p1: EventChannel.EventSink?) {
print("---onListen---")
p1?.success("这是插件主动传值的内容")
}
/**
* EventChannel通道取消后会执行该方法
*/
override fun onCancel(p0: Any?) {
print("---onCancel---")
}
override fun onMethodCall(call: MethodCall, result: Result) {
if (call.method == "getPlatformVersion") {
//当flutter端调用方法getPlatformVersion时,返回当前系统的版本号:Android + 系统版本号
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
//没有实现flutter调用的方法时的回调
result.notImplemented()
}
}
}
然后在main.dart中注册EventChannel的监听:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
// This widget is the root of your application.
String _msg = "这是内容";
//定义MethodChannel通信通道 名称与插件中的一致
static const platform = const MethodChannel("flutter_plugin_test");
//定义EventChannel通信通道 名称与插件中的一致
static const eventChannel = const EventChannel("flutter_plugin_event");
//异步调用插件方法 获取返回内容
Future<void> _getMsg() async{
String msg ;
try{
msg = await platform.invokeMethod("getPlatformVersion");
}on PlatformException{
msg = "获取插件信息错误";
}
if(!mounted){
return;
}
setState(() {
_msg = "这是重插件获取的信息:"+msg;
});
}
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: "这是标题",
home: new Scaffold(
appBar: new AppBar(
title: new Center(
child: new Text("这是居中标题"),
)),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(_msg),
new MaterialButton(
onPressed: () {
_getPluginMsg();
},
child: new Text("获取插件信息"),
color: Colors.blue,
textColor: Colors.white,
),
new MaterialButton(
onPressed: () {
_registerEventChannel();
},
child: new Text("注册EventChannel监听"),
color: Colors.blue,
textColor: Colors.white,
)
],
)
)
),
);
}
_registerEventChannel() {
//注册接收EventChannel传递的数据
eventChannel.receiveBroadcastStream().listen(_onDone,onError: _onError);
}
_onDone(data){
//接收成功
print("EventChannel success:"+data);
}
_onError(data){
//接收失败
print("EventChannel error:"+data);
}
_getPluginMsg() {
print("点击了按钮");
//点击按钮是 获取插件返回值
_getMsg();
}
}
重新运行一下程序,然后再点击 “注册EventChannel监听” 按钮,控制台打印信息如下:
不过这样好像看不到直观效果,我们再修改一下插件中的onListen方法,用定时器给Flutter传值:
override fun onListen(p0: Any?, p1: EventChannel.EventSink?) {
print("---onListen---")
var count = 1
//设置定时器给Flutter传值
val scheduledExecutorService = ScheduledThreadPoolExecutor(1)
val timerTask = timerTask {
//在主线程中运行 否则接收不到数据
activity.runOnUiThread {
p1?.success("插件主动传值 count:$count")
count++
}
}
scheduledExecutorService.scheduleWithFixedDelay(timerTask, 10, 1000, TimeUnit.MILLISECONDS)
}
再运行一下,点击按钮查看效果:
EventChannel可以用于android用电量广播或蓝牙广播接收主动传递给Flutter,感兴趣的同学可以自己试试,在onListen方法中注册广播并传递EventChannel.EventSink作为参数即可。
还有需要注意每一次修改插件后运行主程序时,点击热重载按钮是没有效果的,若程序仍在运行,则需先点击停止按钮再点击运行按钮,这样插件的修改才会生效。