前言
Flutter的UI和状态管理都学了,是时候搞一下混合开发。网上大部分的资料写的都很片面,达不到实战的效果。我觉得混合开发至少要达到以下几个效果
- 原生跳转Flutter
- Flutter跳转原生
- 跳转的时候有数据的交流
本篇主要是以android为主,在现有的工程基础上接入Flutter,ios混合开发步骤大同小异,可以做为参考。
混合开发主要分为两大步骤
- 创建Flutter Module
- 接入Flutter Boost
闲鱼的Flutter-Boost可能是目前最好的混合开发框架,对于内存的把控以及跳转Flutter页面白屏都有很好的处理。
在这里多说一句,如果使用传统的方式从原生跳转到Flutter,白屏现象会非常严重,即便是在release模式下我感觉也不太能接受,毕竟还要兼顾低端手机,不能老是拿着骁龙855去测试呀!
1.创建Flutter Module
整个方案还是参考了官方建议的方式,闲鱼给出aar方案看上去确实很不错,而且对于原生的耦合度会低很多,但是上手成本较高,有兴趣的同学可以自己去了解。
1.1 创建module
1.1.1 在终端里将路径切换到项目根目录,然后使用命令行创建一个flutter module,创建命令如下
flutter create -t module my_flutter
其中“my_flutter”是module的名字,这个可以自己去定义
1.1.2 在原有项目的setting.gradle末尾中添加
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir,
'my_flutter/.android/include_flutter.groovy'
))
include ':my_flutter'
1.1.3 在原生项目app的gradle下添加如下代码
implementation project(":flutter")
经过上面三步原生项目就可以使用Flutter的核心功能了!创建成功后的项目结构如下
注:minSdk必须>=16,否则创建的时候会报错!
2.接入Flutter-Boost
只有接入了跳转功能,一个app的静脉才算是打通,不然就没啥luan用了。原生的跳转有很大的弊端,在Flutter页面和原生页面重叠时,每开启一个Flutter页面就创建了一个Flutter Engine,导致内存暴增,Flutter应用中全局变量在各独立页面不能共享,IOS出现内存泄漏。随着版本的迭代,Flutter-Boost把这些问题都解决了,所以是混合开发的不二之选。
项目地址:Flutter-Boost
2.1 如何接入
如果你的项目还在使用support包,在pubspec.yaml中添加如下依赖
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '0.1.61'
如果你的项目已经切换到androidX,则使用以下方式
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: 'feature/flutter_1.9_androidx_upgrade'
注意:如果Flutter-Boost使用0.1.61版本,对应的Flutter版本不应该低于1.9.1-hotfix,否则会报错。如果你的Flutter分支没有在stable,强烈建议你切换到stable分支上。
在终端使用flutter doctor查看当前分支
如果没有在stable分支,在使用flutter channel查看所有的分支
使用命令flutter channel stable/beta/dev/master切换到对应的分支
2.2 Flutter集成FlutterBoost
2.2.1 创建两个Flutter页面用做跳转,在集成了FlutterBoost后我们所有的跳转动作都可以使用该框架
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'package:my_flutter/constant.dart';
///第一个页面
class FirstRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter Page'),
),
body: Container(
alignment: Alignment.center,
child: RaisedButton(
child: Text('支付吧,皮卡丘'),
onPressed: () {
print("open pay page!");
//利用FlutterBoost跳转到支付页面,并携带参数
FlutterBoost.singleton.open(Routes.ROUTE_NATIVE_PAY,
urlParams: {"need_money": '0.01'});
},
),
),
);
}
}
///第二个页面
class SecondRouteWidget extends StatelessWidget {
final Map params;
SecondRouteWidget(this.params);
@override
Widget build(BuildContext context) {
print(params);
return Scaffold(
appBar: AppBar(
title: Text("支付完成页面-Flutter"),
),
body: Center(
child: RaisedButton(
onPressed: () {
//关闭当前页面并返回一个result
BoostContainerSettings settings =
BoostContainer.of(context).settings;
FlutterBoost.singleton.close(settings.uniqueId,
result: {"result": "data from second"});
},
child: Text('支付结果:${params['result']}'),
),
),
);
}
}
2.2.2 改造main.dart
import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart';
import 'package:my_flutter/constant.dart';
import 'package:my_flutter/simple_page.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
//将需要进行跳转的页面事先注册到FlutterBoost
//pageName是路由名称,params是参数
FlutterBoost.singleton.registerPageBuilders({
Routes.ROUTE_FIRST: (pageName, params, _) => FirstRouteWidget(),
Routes.ROUTE_SECOND: (pageName, params, _) => SecondRouteWidget(params),
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
builder: FlutterBoost.init(postPush: _onRoutePushed),
home: Container(),
);
}
///从原生跳转到Flutter的动作都会经过这里,有需要的话可以在这里做一些统一的操作
void _onRoutePushed(
String pageName, String uniqueId, Map params, Route route, Future _) {}
}
经过上面两步,Flutter这一侧的集成就算是完事了。如果你想使用FlutterBoost做跳转,切记将页面注册到FlutterBoost,不然跳转的时候会报错。下面我们再来看看Native侧要做那些操作。
2.3 Native集成FlutterBoost
在Flutter端集成FlutterBoost后项目结构会发生变化,最终效果如下
此时我们在app的build.gradle中添加如下依赖
implementation project(path: ':flutter_boost')
这样一来我们原生工程也能使用FlutterBoost了,在真正使用以前还需要配置一些东西。
2.3.1 PageRouter-跳转中心
这个类是用来处理原生和Flutter页面跳转逻辑的,具体的业务逻辑都包含在openPageByUrl里面,需要注意的是我的项目用的是androidX,如果你还在使用support包在类的使用上会有些许不同。
package com.zljtech.auction.flutter;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
import com.huodao.platformsdk.logic.core.route.auction.IAuctionRouteUri;
import com.huodao.platformsdk.util.launch.ActivityLaunchUtils;
import com.idlefish.flutterboost.containers.NewBoostFlutterActivity;
import java.util.HashMap;
import java.util.Map;
/**
* @author sq
* @date 2019/12/10
* @describe
*/
public class PageRouter {
public static final String NATIVE_PAY_PAGE_URL = "sample://nativePayPage";
public static final String FLUTTER_PAGE_FIRST_URL = "first";
public static final String FLUTTER_PAGE_SECOND_URL = "second";
/**
* 需要将flutter页面的url放入到该map,这样在跳转的时候才能找到对应的链接
*/
public final static Map<String, String> PAGE_NAME = new HashMap<String, String>() {{
put(FLUTTER_PAGE_FIRST_URL, FLUTTER_PAGE_FIRST_URL);
put(FLUTTER_PAGE_SECOND_URL, FLUTTER_PAGE_SECOND_URL);
}};
public static boolean openPageByUrl(Context context, String url, Map params) {
return openPageByUrl(context, url, params, 0);
}
/**
*
* @param context
* @param url 跳转链接-对应具体的页面
* @param params 携带的参数
* @param requestCode 请求code,一般配合着startActivityForResult使用
* @return
*/
public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {
String path = url.split("\\?")[0];
Log.i("openPageByUrl", path);
try {
if (PAGE_NAME.containsKey(path)) {
//所有Flutter页面的跳转都会走到这里,通过容器NewBoostFlutterActivity来装载
Intent intent = NewBoostFlutterActivity.withNewEngine().url(PAGE_NAME.get(path)).params(params)
.backgroundMode(NewBoostFlutterActivity.BackgroundMode.opaque).build(context);
if (context instanceof Activity) {
Activity activity = (Activity) context;
activity.startActivityForResult(intent, requestCode);
} else {
context.startActivity(intent);
}
return true;
} else if (url.startsWith(NATIVE_PAY_PAGE_URL)) {
String needMoney = (String) params.get("need_money");
//跳转到原生页面
//通过Arouter进行跳转,也可以使用原生Intent
ActivityLaunchUtils.getInstance()
.build(IAuctionRouteUri.ROUTE_AUCTION_ACTIVITY_PAY__ORDER_DEPOSIT)
.withString("need_money", needMoney)
.launch();
return true;
}
return false;
} catch (Throwable t) {
return false;
}
}
}
2.3.2 在Application中配置FlutterBoost
这里主要是做Flutter引擎初始化工作,在Flutter跳转原生的回调方法中实现相应的跳转逻辑。
private void initFlutterBoost() {
INativeRouter router = (context, url, urlParams, requestCode, exts) -> {
String assembleUrl = Utils.assembleUrl(url, urlParams);
PageRouter.openPageByUrl(context, assembleUrl, urlParams);
};
Platform platform = new NewFlutterBoost.ConfigBuilder(this, router)
.isDebug(true)
//当任意activity创建的时候就初始化flutter引擎,防止打开flutter页面时白屏
.whenEngineStart(NewFlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
.renderMode(FlutterView.RenderMode.texture)
.build();
NewFlutterBoost.instance().init(platform);
}
该方法在Application的onCreate调用即可!
2.3.2 AndroidManifest中配置
NewBoostFlutterActivity是Flutter View的承载页,所以这个页面必须在manifest中注册
<activity
android:name="com.idlefish.flutterboost.containers.NewBoostFlutterActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:theme="@style/Theme.AppCompat"
android:windowSoftInputMode="adjustResize">
<!-- 启动页面的过渡动画-->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/anim_loading_view" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
记得在application标签中添加我们自定义的application,这样FlutterBoost才会初始化。
总结
通过上面的步骤我们就完成了FlutterBoost在Native和Flutter端的集成,具体的跳转方法如下:
1.flutter跳转到原生(携带参数)
传递的urlParams对应于PageRouter->openPageByUrl的params参数
FlutterBoost.singleton.open(Routes.ROUTE_NATIVE_PAY,
urlParams: {"need_money": '0.01'});
2.原生跳转Flutter
Map<String, String> params = new HashMap<>();
params.put("result", "pay success");
PageRouter.openPageByUrl(this, PageRouter.FLUTTER_PAGE_SECOND_URL, params);
传递给SecondRouteWidget的参数可以在其构造方法中获取,具体请参照2.2.1
3.Flutter页面退出到原生页面携带返回值
一开始我想的是在close当前Flutter页面的时候,native侧用onActivityResult去接收返回值,毕竟native跳转到flutter的时候是使用startActivityForResult
FlutterBoost.singleton.close(settings.uniqueId,
result: {"result": "data from second"});
这时我们再到native层debug看一下数据:
返回的数据结构有点奇怪,但毕竟是人家定义的,只能照着做了。
- 首先我们通过data获取到一个Bundle
- bundle中包含了一个map,key是“_flutter_result_”, value仍然是一个map
- value里面的数据才是我们真正传递的键值对,这时候再通过我们自己定义的“result”去拿数据就可以了
4.Flutter页面返回Flutter页面携带参数
如果你想使用FlutterBoost进行Flutter页面间的跳转,传递参数什么的会简单很多,话不多说,上代码:
首先从FirstPage跳转到SecondPage
FlutterBoost.singleton.open(Routes.ROUTE_SECOND,
urlParams: {'result': '我是第一个页面携带过来的参数'}).then((result) {
print('接收第二个页面返回的数据=$result');
});
urlParams是传递到下一个页面的数据,open方法会返回一个Future,自然而然地我们想到用then去接收返回的数据。
关闭SecondPage并返回数据给FirstPage
print('接收第一个页面传递过来的数据=:$params');
BoostContainerSettings settings =
BoostContainer.of(context).settings;
FlutterBoost.singleton.close(settings.uniqueId,
result: {"result": "data from second"});
result是返回的数据,然后我们通过日志来验证一下操作是否正确
拿到的数据和我们的预期相差无几,无论是主动传递还是被动接收,数据结构都是一个map,我们通过这个map去解析自己想要的数据即可!
好了关于Flutter混合开发就说这么多,本篇侧重于混合开发期间的页面跳转以及数据传递。FlutterBoost还可以把Flutter页面解析成一个Fragment供原生使用,甚至是把原生控件转换为一个widget供Flutter使用,具体的姿势可以参照官网,这里就不再赘述了!
更新:
集成了Flutter-Boost后,页面跳转6起来了,数据传递也6了,但是我想着混合开发肯定会涉及到Flutter调用原生的方法,按理说使用MethodChannel即可,但是在实际使用中却报错了,几经折腾终于找到解决方案:
在初始化FlutterBoost的时会有一个lifecycleListener接口,我们在engineCreate回调中注册自己的Channel就可以了。
一定记得在onEngineCreated回调中注册,不然flutter engine还没有初始化就注册会报错,而且该方法只会回调一次
private void initFlutterBoost() {
INativeRouter router = (context, url, urlParams, requestCode, exts) -> {
String assembleUrl = Utils.assembleUrl(url, urlParams);
PageRouter.openPageByUrl(context, assembleUrl, urlParams);
};
Platform platform = new NewFlutterBoost.ConfigBuilder(this, router)
.isDebug(true)
.whenEngineStart(NewFlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
.renderMode(FlutterView.RenderMode.texture)
.lifecycleListener(new NewFlutterBoost.BoostLifecycleListener() {
@Override
public void onEngineCreated() {
Logger2.d(TAG, "onEngineCreated");
//注册本地MethodChannel
BinaryMessenger messenger = new BoostPluginRegistry(NewFlutterBoost.instance().engineProvider()).registrarFor(FlutterNativePlugin.CHANNEL).messenger();
MethodChannel methodChannel = new MethodChannel(messenger, FlutterNativePlugin.CHANNEL);
FlutterNativePlugin.registerWith(methodChannel);
}
@Override
public void onPluginsRegistered() {
Logger2.d(TAG, "onPluginsRegistered");
}
@Override
public void onEngineDestroy() {
}
})
.build();
NewFlutterBoost.instance().init(platform);
}
下面是FlutterNativePlugin的具体实现,说白了就是一个MethodChannel
public class FlutterNativePlugin implements MethodChannel.MethodCallHandler {
private static final String TAG = "FlutterNativePlugin";
public static final String CHANNEL = "com.zljtech.channel.method";
private FlutterNativePlugin() {
}
public static void registerWith(MethodChannel channel) {
FlutterNativePlugin instance = new FlutterNativePlugin();
channel.setMethodCallHandler(instance);
}
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if ("isOk".equals(methodCall.method)) {
boolean isOk = Math.random() > 0.5;
Logger2.d(TAG, "isOk====" + isOk);
result.success(isOk);
} else {
result.notImplemented();
}
}
}
然后就可以愉快的在Flutter中调用Native方法啦!