本文主要解決3個(gè)問題:
- 集成Flutter到Android項(xiàng)目,可以打開Flutter的默認(rèn)頁(yè)面
- 可以跳轉(zhuǎn)到Flutter的指定頁(yè)面
- 可以將Flutter的指定組件嵌入到原生頁(yè)面,并傳遞參數(shù)
1.集成Flutter到Android
這里,我們以Flutter Module創(chuàng)建一個(gè)Flutter工程(flutter),然后run起來(lái),就可以在.android/Flutter/build/outouts/aar文件夾下面得到這個(gè)aar
這里之所以以Flutter Module模式開發(fā),而不是Flutter Application,就是為了得到這個(gè)aar。
Flutter Module模式下自動(dòng)生成的.android文件夾下,才會(huì)有這個(gè)Flutter文件夾,F(xiàn)lutter Application則沒有。
這樣的話,我們才可以借用Flutter已經(jīng)有的生成aar的gradle腳本,不然還得自己去寫gradle打包腳本,很容易踩到坑里就爬不起來(lái)了。
然后我們?cè)倭黹_一個(gè)窗口,新建一個(gè)Android工程(flutter_container),將這個(gè)aar復(fù)制過(guò)去
這里需要注意的一個(gè)問題,因?yàn)镕lutter本身原因,導(dǎo)致復(fù)制出來(lái)的aar里面缺少icudtl.dat文件,需要我們自己手動(dòng)復(fù)制這個(gè)icudtl.dat文件到assets/flutter_shared目錄下。
怎么得到這個(gè)icudtl.dat文件呢,很簡(jiǎn)單,解壓Flutter工程生成的默認(rèn)apk即可得到
然后,我們就需要在宿主Android工程里面,建立接收Flutter的Activity了。這里可以借鑒Flutter工程的.android/app目錄,核心就是兩個(gè):
- Application:初始化Flutter
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
FlutterMain.startInitialization(this);
}
}
- Activity:繼承FlutterActivity
/**
* debug模式原生跳轉(zhuǎn)到flutter界面會(huì)出現(xiàn)白屏,release包就不會(huì)出現(xiàn)白屏了
*/
public class MainFlutterActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
}
這樣以后,我們就可以跳轉(zhuǎn)這個(gè)MainFlutterActivity,實(shí)現(xiàn)在Android工程里面進(jìn)入Flutter工程的默認(rèn)頁(yè)面了。
2. 跳轉(zhuǎn)指定頁(yè)面
上面只是簡(jiǎn)單集成了Flutter,但是我們知道,我們從Android工程里面跳轉(zhuǎn)Flutter,肯定是需要選擇性的跳轉(zhuǎn)指定頁(yè)面的,不可能只是簡(jiǎn)單的跳轉(zhuǎn)默認(rèn)頁(yè)面就完了,所以,這里需要用到Flutter的靜態(tài)路由了。
修改Flutter工程的main.dart,定義了兩個(gè)指定頁(yè)面的路由:homePage、channelPage
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: TestPage(),
//這種方式不能傳遞參數(shù),主要是方便原生調(diào)用
routes: <String, WidgetBuilder> {
'homePage': (BuildContext context) => new HomePage(),
'channelPage': (BuildContext context) => new ChannelPage(),
},
);
}
}
然后在宿主Android工程下,添加指定頁(yè)面的容器Activity,通過(guò)Flutter.createView來(lái)獲取指定頁(yè)面的View
注意,這里的HomeFlutterActivity只需要繼承AppCompatActivity 即可,不需要繼承FlutterActivity了。
public class HomeFlutterActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FlutterView homePage = Flutter.createView(
this,
getLifecycle(),
"homePage"
);
setContentView(homePage);
}
}
這樣以后,我們就可以跳轉(zhuǎn)這個(gè)HomeFlutterActivity,實(shí)現(xiàn)在Android工程里面進(jìn)入Flutter工程的指定頁(yè)面了。
3. 嵌入View并傳遞參數(shù)
上面雖然能夠跳轉(zhuǎn)指定頁(yè)面了,但是很顯然,有一個(gè)很大的問題:不能傳遞參數(shù)。
這是Flutter的靜態(tài)路由的一個(gè)很大的弊端,雖然通過(guò)動(dòng)態(tài)路由可以傳遞參數(shù)和接收返回值,但是動(dòng)態(tài)路由沒法給原生調(diào)用。
Navigator.of(context)
.push<String>(new MaterialPageRoute(builder: (context) {
return new NextPage(params);
})).then((String value) {
setState(() {
params = value;
});
});
有一個(gè)Flutter的路由庫(kù):Fluro,可以實(shí)現(xiàn)靜態(tài)路由傳參,例如這樣:
傳參
var bodyJson = '{"user":1281,"pass":3041}';
router.navigateTo(context, '/home/$bodyJson');
接收
Router router = new Router();
void main() {
router.define('/home/:data', handler: new Handler(
handlerFunc: (BuildContext context, Map<String, dynamic> params) {
return new FluroHomePage(params['data'][0]);
}));
runApp(MyApp());
}
但是,這種方式在Flutter內(nèi)部還行,卻無(wú)法給原生調(diào)用,在原生里面通過(guò)Flutter.createView的時(shí)候,是沒法使用Fluro的,只能是默認(rèn)的路由。
調(diào)研了很多方案,最后,沒有辦法了,只好采用最笨的方法:通過(guò)MethodChannel來(lái)傳遞參數(shù)。
這里需要注意的是MethodChannel的調(diào)用,應(yīng)該FlutterView已經(jīng)創(chuàng)建完成,所以需要通過(guò)flutterView.post(new Runnable())來(lái)執(zhí)行了,直接執(zhí)行是不會(huì)傳參給Flutter的。
原生傳參給Flutter
原生調(diào)用
MethodChannel channel = new MethodChannel(flutterView, CHANNEL);
channel.invokeMethod("invokeFlutterMethod", "hello,flutter", new MethodChannel.Result() {
@Override
public void success(@Nullable Object o) {
Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-success:"+o.toString());
}
@Override
public void error(String s, @Nullable String s1, @Nullable Object o) {
Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-error");
}
@Override
public void notImplemented() {
Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-notImplemented");
}
});
Flutter執(zhí)行
platform.setMethodCallHandler((handler) {
Future<String> future=Future((){
switch (handler.method) {
case "invokeFlutterMethod":
String args = handler.arguments;
print("2.Flutter執(zhí)行invokeFlutterMethod:${args}");
return "this is flutter result";
}
});
return future;
});
Flutter傳參給原生
Flutter調(diào)用
print("3.Flutter調(diào)用invokeNativeMethod");
int result =
await platform.invokeMethod("invokeNativeMethod", "hello,native");
print("5.收到原生執(zhí)行結(jié)果:${result}");
原生執(zhí)行
channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method) {
case "invokeNativeMethod":
String args = (String) call.arguments;
Log.i("flutter","4.原生執(zhí)行invokeNativeMethod:"+args);
result.success(200);
break;
default:
}
}
});
最后貼一下這個(gè)傳參頁(yè)面的完整代碼吧,主要就是跑了一下:
- 原生調(diào)用invokeFlutterMethod
- Flutter執(zhí)行invokeFlutterMethod
- Flutter調(diào)用invokeNativeMethod
- 原生執(zhí)行invokeNativeMethod
Android:
public class ChannelFlutterActivity extends AppCompatActivity {
private static final String CHANNEL = "com.ezbuy.flutter";
FlutterView flutterView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_channel);
FrameLayout frFlutter = findViewById(R.id.fr_flutter);
flutterView = getFlutterView("channelPage");
frFlutter.addView(flutterView);
flutterView.post(new Runnable() {
@Override
public void run() {
initMethodChannel(flutterView);
}
});
}
public FlutterView initMethodChannel(FlutterView flutterView) {
MethodChannel channel = new MethodChannel(flutterView, CHANNEL);
//1.原生調(diào)用Flutter方法
channel.invokeMethod("invokeFlutterMethod", "hello,flutter", new MethodChannel.Result() {
@Override
public void success(@Nullable Object o) {
Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-success:"+o.toString());
}
@Override
public void error(String s, @Nullable String s1, @Nullable Object o) {
Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-error");
}
@Override
public void notImplemented() {
Log.i("flutter","1.原生調(diào)用invokeFlutterMethod-notImplemented");
}
});
Log.i("flutter","1.原生調(diào)用invokeFlutterMethod");
//4.Flutter調(diào)用原生方法的監(jiān)聽
channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
switch (call.method) {
case "invokeNativeMethod":
String args = (String) call.arguments;
Log.i("flutter","4.原生執(zhí)行invokeNativeMethod:"+args);
result.success(200);
break;
default:
}
}
});
return flutterView;
}
public FlutterView getFlutterView(String initialRoute) {
return Flutter.createView(
this,
getLifecycle(),
initialRoute
);
}
}
Flutter
class ChannelPage extends StatefulWidget {
ChannelPage();
@override
_ChannelPageState createState() => _ChannelPageState();
}
class _ChannelPageState extends State<ChannelPage> {
static const platform = const MethodChannel('com.ezbuy.flutter');
String data;
@override
void initState() {
super.initState();
data ="默認(rèn)data";
initChannel();
}
@override
Widget build(BuildContext context) {
//必須用Scaffold包裹
return Scaffold(body: new Center(child: new Text(data)));
}
void initChannel() {
platform.setMethodCallHandler((handler) {
Future<String> future=Future((){
switch (handler.method) {
case "invokeFlutterMethod":
String args = handler.arguments;
print("2.Flutter執(zhí)行invokeFlutterMethod:${args}");
setState(() {
data = "2.Flutter執(zhí)行invokeFlutterMethod:${args}";
});
invokeNativeMethod();
return "this is flutter result";
}
});
return future;
});
}
void invokeNativeMethod() async {
print("3.Flutter調(diào)用invokeNativeMethod");
int result =
await platform.invokeMethod("invokeNativeMethod", "hello,native");
print("5.收到原生執(zhí)行結(jié)果:${result}");
}
}
對(duì)啦,我們這節(jié)說(shuō)的是將Flutter以View級(jí)別嵌套在一個(gè)Android的Activity里面,其實(shí)很簡(jiǎn)單了啊,因?yàn)槲覀兺ㄟ^(guò)Flutter.createView創(chuàng)建出來(lái)的View和普通的View并沒有什么太大的區(qū)別,直接addView就可以了,沒啥特別操作,比如這個(gè)ChannelFlutterActivity,我用的布局文件就是如下所示:
最后的執(zhí)行效果就是:
其它坑
1. Flutter工程依賴了插件時(shí),宿主Android工程會(huì)報(bào)找不到插件的原生代碼的錯(cuò)誤
我的Flutter工程依賴了shared_preferences插件,導(dǎo)致報(bào)錯(cuò):
原因是:Flutter工程導(dǎo)出成aar的時(shí)候,沒有包含插件里面的原生代碼。
解決方案有2種,網(wǎng)上說(shuō)是不用默認(rèn)的生成aar的方式,用fataar-gradle-plugin來(lái)讓生成的flutter.aar直接包含嵌套的插件工程的aar,這就需要修改Flutter工程的.android/Flutter/build.gradle文件了。我試過(guò),結(jié)果報(bào)了循環(huán)依賴的錯(cuò)誤,就放棄了,大家如果這個(gè)方案走通了,歡迎告知我具體步驟。
我的解決方案:這里我采取了一個(gè)簡(jiǎn)單粗暴直接的方案,直接找到插件的aar,將它也復(fù)制到宿主Android工程了。這個(gè)插件的aar在這里:
復(fù)制到這里:
但是這個(gè)方案的弊端就是,以后每一個(gè)插件,你都需要復(fù)制一下,后期的維護(hù)成本是有點(diǎn)高的。不像fataar是一勞永逸,只有flutter.aar這一份aar的。尤其是后期肯定會(huì)將aar做成遠(yuǎn)程依賴,而不再是直接發(fā)復(fù)制過(guò)去,那維護(hù)成本就更高了些。
結(jié)語(yǔ)
通過(guò)上文可以看到,其實(shí)Flutter集成到Android項(xiàng)目還是挺方便的(除了FlutterView傳參有點(diǎn)麻煩)。至于Flutter如何集成到ios項(xiàng)目,我還沒有實(shí)踐過(guò),還需要和ios的同事探索,如果你在集成到ios項(xiàng)目的過(guò)程中,填了哪些坑,有哪些經(jīng)驗(yàn)總結(jié),歡迎和我們交流。