Flutter、Android混合开发实践
一、前言
Flutter现在如日中天,作为一只iOS程序猿终于下定决心去深入了解这么一种强大的跨平台框架在各个平台上的使用方式,也借此机会了解Android开发。
本着低侵入的原则,将Flutter编译成aar供Android工程依赖的方式无疑是最优解。下面会介绍Flutter、Android混合开发,并将Flutter编译成aar的过程,以及记录本人实践过程中碰到的问题。
二、Android工程添加Flutter工程
功能开发期间将Flutter和Android两个功能同时导入到AndroidStudio,每次编译Android功能即可调试混合工程。待功能开发完成后将Flutter编译成aar文件,导入Android工程。大致步骤如下:
- 步骤一:新建Android工程
- 步骤二:新建Flutter Module
- 步骤三:修改工程配置文件,将Flutter工程引入Android工程
- 步骤四:编写测试代码,编译两个工程查看结果
步骤一:新建Android工程
先在android目录下新建Android project ,选择新建Android Studio project -> 选择Empty Activity,api版本选择4.1(本次实践的环境为4.1),finish。
步骤二:新建Flutter Module
Flutter Module新建有两种方式,其一是通过AndroidStudio新建,其二是通过命令行新建。
- 方法一:命令行
flutter create -t module my_flutter
my_flutter为module的名字,执行完命令等待即可。
- 方法二:使用Android Studio新建


步骤三:修改工程配置文件,将Flutter工程引入Android工程
找到android目录下的build.gradle文件,将默认库地址修改为国内阿里云的maven库地址,防止不科学上网引起的更新龟速问题。
buildscript {
repositories {
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' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
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' }
}
}
找到android/app目录下的build.gradle文件,声明以下源兼容性。
android {
cmpileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}}
在根目录(即android)目录下的setting.gradle文件中加入如下代码:
include ':app'
rootProject.name='myAndroid'
// 加入下面配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'my_flutter/.android/include_flutter.groovy'
))
上面代码中的“my_flutter”为我新建的Flutter Module名称,Sync一下项目。
Sync成功后,我们可以看到项目中多了一个my_flutterd的子项目
Flutter Module添加成功后,我们需要在android/app目录下的build.gradle文件中添加该module的依赖。
implementation project(':flutter')
完成以上步骤,那么恭喜您已经成功将FlutterModule添加到了Android工程中。接下来我们就可以写一些简单的代码,进行Android和Flutter直接的交互了。
步骤四:编写测试代码,编译两个工程查看结果
为了解决大部分场景中的使用情况,我们主要编写的代码为Android和Flutter之间的页面跳转,以及跳转时的传值。在Android工程中,新建Android原生页面FirstNativeActivity、SecondNativeActivity,以及承载Flutter页面的Android套壳原生页面FirstFlutterActivity。下面介绍5种Android和Flutter交互的场景:
- Android页面打开Flutter页面并传值
- Flutter页面打开Android页面并传值
- Android页面退回Flutter页面并传值
- Flutter页面退回Android页面并传值
- 解决手机系统虚拟返回按键破坏正常页面的栈顺序问题
在开始上面的场景前,先在Android 套壳原生页面FirstFlutterActivity引入Flutter页面。并在Flutter页面定义如下内容:
- 添加一个textView用来显示其他页面传过来的内容
- 添加一个button用来打开下个原生页面
- 添加一个button用来返回到上个原生页面
思路:新建FlutterView -> 在xml中拖一个FrameLayout -> 将FlutterView添加到FrameLayout中 -> 创建FlutterEngine,并初始化引擎指向一个Flutter页面的路由-> FlutterView使用FlutterEngine加载内容。上面的介绍和WebView的加载方式如出一辙。
关键名词介绍:
FlutterView:位于io.flutter.embedding.android包中,在Flutter1.12版本里,他负责创建Flutter视图。而且FlutterView继承于FrameLayout,所以上面思路中我们可以把他当作一个基础的View进行操作。
FlutterEngine:Flutter负责在Android端执行Dart代码的引擎,将Flutter编写的UI代码渲染到FlutterView中。
创建FlutterView并添加到视图中代码:
FlutterView flutterView = new FlutterView(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout flContainer = findViewById(R.id.layout001);
flContainer.addView(flutterView, lp);
创建FlutterEngine,并渲染路由名称为route1的Flutter页面,路由可以携带一些数据(字符串:message)。
//创建引擎
flutterEngine = new FlutterEngine(this);
String str = "route1?{\"message\":\"" + message + "\"}";
flutterEngine.getNavigationChannel().setInitialRoute(str);
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
//渲染Flutter页面
flutterView.attachToFlutterEngine(flutterEngine);
Flutter中Dart代码如下:
解析路由获取本次携带的数据
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String url) {
// route名称
String route = url.indexOf('?') == -1 ? url : url.substring(0, url.indexOf('?'));
// 参数Json字符串
String paramsJson = url.indexOf('?') == -1 ? '{}' : url.substring(url.indexOf('?') + 1);
Map<String, dynamic> mapJson = json.decode(paramsJson); String message = mapJson["message"];
// 解析参数
switch (route) {
case 'route1':
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter页面'),
),
body: Center(child: Text('页面名字: $route',style: TextStyle(color: Colors.red), textDirection: TextDirection.ltr),),
),
);
default:
return Center(
child: Text('Unknown route: $route',style: TextStyle(color: Colors.red), textDirection: TextDirection.ltr),
);
}}
完成以上代码就可以在Android壳子中查看Flutter页面,下面介绍壳子页面中Android和Flutter是如何交互的:
思路:我们熟悉的传统的h5页面和原生交互时,通过中间通信工具对象,定义好方法或者属性进行通信。同理,Flutter与Android原生交互也有专门的通信对象(Platform Channel),它有三种类型:
- MethodChannel:用于最常见的方法传递,帮助Flutter和原生平台互相调用方法,也是本次我们着重介绍的。
- BasicMessageChannel:用于数据信息的传递。
- EventChannel:用于事件监听传递等场景。
我们在开始使用MethodChannel时,先对其进行唯一性定义。注意:这里我们定义两个MethodChannel,一个用于对Flutter的消息发送,一个用于Flutter的回调消息接收。
//Flutter向Native发消息
private static final String CHANNEL_NATIVE = "com.example.flutter/native";
//Native向Flutter发消息
private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";
使用定义好的名字,初始化MethodChannel。注意:MethodChannel初始化方法里有两个参数。第一个参数BinaryMessenger messenger,我们可以理解为MethodChannel和Flutter页面的绑定项,通过FlutterEngine的getDartExecutor()方法我们可以得到构造MethodChannel的第一个参数。第二个参数需要传入我们之前定义好的唯一命名。
Android接收Flutter发来的消息
接收Flutter消息得先初始化一个MethodChannel,且用之前定义好的名字CHANNEL_NATIVE。通过下面代码我们可以看到MethodChannel回调参数有:MethodCall call、MethodChannel.Result result。call可以给我们提供本次Flutter所发送的方法名(call.method)。result提供了一些方法可以在我们处理完逻辑后,告诉Flutter页面我们的处理结果,例如result.success()、result.notImplemented()。
MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_NATIVE);
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method){
case "backFirstNative":
result.success("收到来自Flutter的消息");
break;
default :
result.notImplemented();
break;
}
}
});
Android给Flutter发消息
给Flutter发消息同样得先初始化一个MethodChannel,且用之前定义好的名字CHANNEL_FLUTTER。使用MethodChannel的方法invokeMethod就可以将本次的消息发送到Flutter中去啦!
Map<String, Object> result = new HashMap<>();
result.put("message", @"我是Android的发出去的信息,我要到Flutter中去");
MethodChannel flutterChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_FLUTTER);
// 调用Flutter端定义的方法onActivityResult
flutterChannel.invokeMethod("onActivityResult", result);
上面介绍了交互时Android端的代码,下面介绍Flutter端的代码。如下:
首先我们在原来的main.dart文件中做一下扩展。定义一个Widget用来显示Android传过来的数据,并创建一个按钮给Android发消息。同Android端,在main.dart文件中我们也定义了同名MethodChannel。注意:我们在Widget的initState()方法里就应该写上MethodChannel的监听代码。我们可以在Flutter的MethodChannel的回调方法中通过获取call.method、call.method.arguments来知道,Android这次想要调用我们什么方法、以及带来了什么参数。
class ContentWidget extends StatefulWidget{
ContentWidget({Key key, this.route,this.message}) : super(key: key);
String route,message;
_ContentWidgetState createState() => new _ContentWidgetState();
}
class _ContentWidgetState extends State<ContentWidget>{
static const nativeChannel = const MethodChannel('com.example.flutter/native');
static const flutterChannel = const MethodChannel('com.example.flutter/flutter');
void onDataChange(val) {
setState(() {
widget.message = val;
});
}
@override
void initState(){
super.initState();
Future<dynamic> handler(MethodCall call) async{
switch (call.method){
case 'onActivityResult':
onDataChange(call.arguments['message']);
print('1234'+call.arguments['message']);
break;
}
}
flutterChannel.setMethodCallHandler(handler);
}
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: Stack(
children: <Widget>[
Positioned(
top: 100,
left: 0,
right: 0,
height: 100,
child: Text(widget.message,textAlign: TextAlign.center,),
),
Positioned(
top: 300,
left: 100,
right: 100,
height: 100,
child: RaisedButton(
child: Text('打开上一个原生页面'),
onPressed: (){
returnLastNativePage(nativeChannel);
}
),
),
Positioned(
top: 430,
left: 100,
right: 100,
height: 100,
child: RaisedButton(
child: Text('打开下一个原生页面'),
onPressed: (){
openNextNativePage(nativeChannel);
}
),
)
],
),
);
}}
上面的代码缺少了方法:returnLastNativePage、openNextNativePage。如下:
大家肯定还记得我们之前在Android页面接收Flutter的回调后,还能调用result.success()来告诉Flutter页面我们的处理结果。没错,我们在下面两个方法中,异步获取这些回调的信息并打印。
Future<Null> returnLastNativePage(MethodChannel channel) async{
Map<String, dynamic> para = {'message':'嗨,本文案来自Flutter页面,回到第一个原生页面将看到我'};
final String result = await channel.invokeMethod('backFirstNative',para);
print('这是在flutter中打印的'+ result);
}
Future<Null> openNextNativePage(MethodChannel channel) async{
Map<String, dynamic> para = {'message':'嗨,本文案来自Flutter页面,打开第二个原生页面将看到我'};
final String result = await channel.invokeMethod('openSecondNative',para);
print('这是在flutter中打印的'+ result);
}
至此,Android、Flutter可以互通有无了。如果你在编译的时候发现main.dart中MethodChannel报错,那么你一定是没有正确的引入头文件比如:import 'package:flutter/services.dart'。之前介绍的那些跳转场景到最后都变成了Android之间的跳转,以及壳子页面对Flutter的更新。下面科普一下Android Activity之间跳转的传值。有Android基础的朋友可以直接下跳到最后查看:解决手机系统虚拟返回按钮破坏正常页面的栈顺序。
下面简单介绍Android页面之间的跳转。注意:请使用startActivityForResult方法打开Activity,这样Activity关闭时onActivityResult方法才能接收到回调。代码如下:
FirstFlutterActivity代码:
打开下一个Activity。
// 跳转原生页面
Intent jumpToNativeIntent = new Intent(FirstFlutterActivity.this, SecondNativeActivity.class);
jumpToNativeIntent.putExtra("message", (String) call.argument("message"));
startActivityForResult(jumpToNativeIntent, Activity.RESULT_FIRST_USER);
result.success("成功打开第二个原生页面");
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case 1:
if (data != null) {
// NativePageActivity返回的数据
String message = data.getStringExtra("message");
Map<String, Object> result = new HashMap<>();
result.put("message", message);
// 创建MethodChannel,这里的flutterView即Flutter.createView所返回的View
MethodChannel flutterChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_FLUTTER);
// 调用Flutter端定义的方法
flutterChannel.invokeMethod("onActivityResult", result);
}
break;
default:
break;
}}
SecondNativeActivity代码:
xml里拖了一个id为textView2的textView用来显示信息,一个id为button001的button用来返回上个页面。
public class SecondNativeActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second_native);
Intent intent = getIntent();
String content = intent.getStringExtra("message");
TextView textView = findViewById(R.id.textView2);
textView.setText(content);
Button btnOpen = findViewById(R.id.button001);
btnOpen.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent();
intent.putExtra("message","嗨,本文案来自第二个原生页面,将在Flutter页面看到我");
setResult(RESULT_OK,intent);
finish();
}
});
}}
解决手机系统虚拟返回按键破坏正常页面的栈顺序问题
场景:原生页面A -> 打开原生壳子页面(显示内容:Flutter页面B) -> 打开原生壳子页面(显示内容:Flutter页面C)-> 点击虚拟返回按钮
现象:原生壳子页面(显示内容:Flutter页面C)直接回到了原生页面A。
上面的现象和一个不经处理的WebView页面栈管理如出一辙,那不是我们像要的。我们想达到点击虚拟返回按键后,原生壳子页面(显示内容:Flutter页面C)回到 原生壳子页面(显示内容:Flutter页面B)。
思路:如果将点击虚拟返回按钮的后续操作交给Flutter来处理,那么就完美。
我们知道点击虚拟返回按钮的后将会调用方法onBackPressed(),此时我们在该方法中给Flutter发消息(即调用"backAction"方法)。
@Override
public void onBackPressed() {
MethodChannel flutterChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_FLUTTER);
flutterChannel.invokeMethod("backAction", null);
}
Flutter中的处理:
和之前交互的处理类似,我们增加了一个case 'backAction',这里要用到Flutter里的一个方法canPop()。我们知道如果是栈底了还调用pop()方法会使程序crash,canPop()就很好的解决了这个问题。当在栈底调用canPop()时,会返回给我们一个布尔值告诉我们是否可以继续回退。当我们发现canPop()的结果是false时,说明当前Flutter页面已经是最后一个页面,此时我们应该通知Android壳子页面退回到上一个原生页面。代码如下:
void initState(){
super.initState();
Future<dynamic> handler(MethodCall call) async{
switch (call.method){
case 'onActivityResult':
onDataChange(call.arguments['message']);
print('1234'+call.arguments['message']);
break;
case 'backAction':
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
} else {
nativeChannel.invokeMethod('backAction');
}
break;
}
}
flutterChannel.setMethodCallHandler(handler);
}
Android壳子页面接收到消息,返回上一个原生页面
MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), CHANNEL_NATIVE);
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method){
case "backFirstNative":
result.success("收到来自Flutter的消息");
break;
case "backAction":
finish();
result.success("成功通过虚拟按键返回第一个原生页面");
break;
default :
result.notImplemented();
break;
}
}
});
三、打包Flutter工程,以aar的形式供Android调用
在Flutter1.12版本中,打包Flutter已经变得十分简单,在完成Flutter代码编写后,运行命令行:
flutter build aar
或者点击Build -> Flutter -> Build AAR即可
当你使用命令行打包时,打包完成后控制台会提示你如何使用aar
如上图所以,就是让你在想要引用aar的Android工程下,找到android/app/build.gradle文件。并把上图中的2、3、4点提到的代码加入其中。添加完代码后Sync一下,运行工程查看正确结果。
上面的尝试都是基于Flutter1.12版本实现,若您的Flutter版本 < 1.12,请先更新Flutter版本。
-----------------------------------完整代码地址---------------------------------------------
https://github.com/JJwow/Android_Flutter_MixBuilder.git
转载自:https://juejin.cn/post/6844904065881604109