前言
目前Flutter可以說(shuō)是非?;馃崃?,多次更新過(guò)后也越來(lái)越穩(wěn)定,受到了很多開(kāi)發(fā)者的青睞。不過(guò)純Flutter開(kāi)發(fā)還是存在一定成本和風(fēng)險(xiǎn)的,尤其是對(duì)于規(guī)模稍大一些的項(xiàng)目,可能更加適合的是將Flutter用于項(xiàng)目中的某一個(gè)模塊,因此我們有必要了解一下如何在原生項(xiàng)目中引入Flutter。
本文介紹一下Android原生項(xiàng)目引入Flutter的方法以及Flutter如何與原生進(jìn)行交互,包括頁(yè)面間的跳轉(zhuǎn)和方法的調(diào)用,本人不懂IOS開(kāi)發(fā),有需要的話還是自行百度吧o(╥﹏╥)o,但是基本思路我覺(jué)得不會(huì)差太多的。
Android原生項(xiàng)目中引入Flutter
這應(yīng)該是目前Flutter在實(shí)際開(kāi)發(fā)中應(yīng)用最多的一種場(chǎng)景,在已有的Android原生項(xiàng)目中引入Flutter,針對(duì)一些復(fù)雜的頁(yè)面,使用Flutter開(kāi)發(fā)可以有效地提高開(kāi)發(fā)效率。
官方提供的文檔Add Flutter to existing apps詳細(xì)介紹了原生app引入Flutter的步驟,不過(guò)很遺憾是英文的。我也是參考了網(wǎng)上的一些相關(guān)文章,總結(jié)了一下文檔中的提到的幾個(gè)步驟。
- 第一步、新建Android項(xiàng)目
這沒(méi)什么可說(shuō)的,畢竟我們是要在原生項(xiàng)目中引入Flutter嘛。
- 第二步、新建Flutter Module
有兩種方式來(lái)創(chuàng)建Flutter Module,第一種是通過(guò)命令行來(lái)創(chuàng)建,首先切換到Android項(xiàng)目的同級(jí)目錄下,執(zhí)行以下命令:
flutter create -t module my_flutter
其中my_flutter為module的名字。第二種是直接使用Android Studio來(lái)創(chuàng)建,依次點(diǎn)擊左上角的File --> New --> New Flutter Project,然后選擇Flutter Module。
然后填寫module的名稱、路徑。
最后填寫module的包名,點(diǎn)擊Finish就創(chuàng)建好了一個(gè)Flutter Module。
- 第三步、在Android項(xiàng)目中引入Flutter Module
首先在app下的build.gradle文件中添加以下配置:
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
我們知道這是使用Java 8所需要的配置,在這里的作用是為了解決版本兼容問(wèn)題,如果不配置的話運(yùn)行項(xiàng)目可能會(huì)報(bào)錯(cuò):Invoke-customs are only supported starting with Android O (--min-api 26)。
然后在項(xiàng)目根目錄下的setting.gradle文件中配置:
include ':app'
// 加入下面配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'my_flutter/.android/include_flutter.groovy'
))
記得修改成自己的Flutter Module名稱,之后Sync一下項(xiàng)目。Binding可能會(huì)因?yàn)檎也坏蕉鴺?biāo)紅,我沒(méi)有導(dǎo)包最后也可以Sync成功,并不影響module的引入,這一點(diǎn)我還不清楚是什么原因,如果有知道的小伙伴歡迎提出。
Sync后我們可以看到項(xiàng)目中多了一個(gè)名稱為flutter的library module,我們需要在app下的build.gradle文件中添加該module的依賴。
implementation project(':flutter')
這樣就成功地將Flutter引入到了Android原生項(xiàng)目中。
Android和Flutter的交互
通過(guò)上面的幾個(gè)步驟我們已經(jīng)在Android原生項(xiàng)目中集成了Flutter,之后就需要解決交互問(wèn)題了。首先介紹一下Android頁(yè)面和Flutter頁(yè)面之間的跳轉(zhuǎn)。
Tips:由于Flutter版本的更新,下面介紹的內(nèi)容中存在一些API已經(jīng)被廢棄的情況,不過(guò)各種交互場(chǎng)景的處理思路是不變的,關(guān)于Flutter版本變更的內(nèi)容我補(bǔ)充到了文章最后,大家可以結(jié)合起來(lái)看。
Android原生頁(yè)面跳轉(zhuǎn)Flutter頁(yè)面
基本思路就是將Flutter編寫的頁(yè)面嵌入到Activity中,官方提供了兩種方式:通過(guò)FlutterView
和FlutterFragment
,下面我們分別看一下這兩種方式是如何實(shí)現(xiàn)的。
1.使用FlutterView
首先新建一個(gè)Activity,命名為FlutterPageActivity(名稱隨意起),在onCreate()
方法中添加以下代碼:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 通過(guò)FlutterView引入Flutter編寫的頁(yè)面
View flutterView = Flutter.createView(this, getLifecycle(), "route1");
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(600, 800);
layout.leftMargin = 100;
layout.topMargin = 200;
addContentView(flutterView, layout);
}
Flutter.createView()
方法返回的是一個(gè)FlutterView,它繼承自View,我們可以把它當(dāng)做一個(gè)普通的View,調(diào)用addContentView()
方法將這個(gè)View添加到Activity的contentView中。我們注意到Flutter.createView()
方法的第三個(gè)參數(shù)傳入了"route1"字符串,表示路由名稱,它確定了Flutter中要顯示的Widget,接下來(lái)需要在之前創(chuàng)建好的Flutter Module中編寫邏輯了,修改main.dart文件中的代碼:
import 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String route) {
switch (route) {
case 'route1':
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Flutter頁(yè)面'),
),
body: Center(
child: Text('Flutter頁(yè)面,route=$route'),
),
),
);
default:
return Center(
child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
);
}
}
在runApp()
方法中通過(guò)window.defaultRouteName
可以獲取到我們?cè)?code>Flutter.createView()方法中傳入的路由名稱,即"route1",之后編寫了一個(gè)_widgetForRoute()
方法,根據(jù)傳入的route字符串顯示相應(yīng)的Widget。
最后在MainActivity中添加一個(gè)Button,編寫點(diǎn)擊事件,點(diǎn)擊Button跳轉(zhuǎn)到FlutterPageActivity。
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btnJumpToFlutter = findViewById(R.id.btn_jump_to_flutter);
btnJumpToFlutter.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, FlutterPageActivity.class);
startActivity(intent);
}
});
運(yùn)行項(xiàng)目,點(diǎn)擊MainActivity中的Button跳轉(zhuǎn)到FlutterPageActivity,效果如下圖所示:
可以看到我們已經(jīng)成功地將Flutter編寫的Widget嵌入到了Activity中,為了更逼真一些,還需要做一些調(diào)整。首先修改LayoutParams參數(shù),將View占滿屏幕。
View flutterView = Flutter.createView(this, getLifecycle(), "route1");
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
addContentView(flutterView, layout);
然后需要隱藏原生的標(biāo)題欄,在資源文件夾res/values中的style.xml文件中添加一個(gè)FlutterPageTheme。
<style name="FlutterPageTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!--狀態(tài)欄透明-->
<item name="android:windowTranslucentStatus">true</item>
</style>
然后在AndroidManifest.xml文件中設(shè)置Activity的Theme。
<activity
android:name=".FlutterPageActivity"
android:theme="@style/FlutterPageTheme" />
再次運(yùn)行項(xiàng)目看一下效果,這樣就自然多了,當(dāng)然我們還可以繼續(xù)修改標(biāo)題欄的背景顏色,這里就不提了。
2.使用FlutterFragment
為了簡(jiǎn)單,我們依然使用FlutterPageActivity,新建一個(gè)布局文件activity_flutter_page:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
修改onCreate()
方法:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flutter_page);
// 通過(guò)FlutterFragment引入Flutter編寫的頁(yè)面
FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
tx.replace(R.id.fl_container, Flutter.createFragment("route1"));
tx.commit();
}
Flutter.createFragment()
方法傳入的參數(shù)同樣表示路由名稱,用于確定Flutter要顯示的Widget,返回一個(gè)FlutterFragment,該類繼承自Fragment,將該Fragment添加到Activity中就可以了。
在調(diào)試時(shí)會(huì)遇到一個(gè)問(wèn)題,顯示出Flutter頁(yè)面之前會(huì)黑屏幾秒,不要擔(dān)心,打了release包后就沒(méi)問(wèn)題了。
如何傳遞參數(shù)跳轉(zhuǎn)
通過(guò)以上兩種方式實(shí)現(xiàn)了將Flutter編寫的頁(yè)面嵌入到Activity中,但是這只是最簡(jiǎn)單的情況,如果我們需要在頁(yè)面跳轉(zhuǎn)時(shí)傳遞參數(shù)呢,如何在Flutter代碼中獲取到原生代碼中的參數(shù)呢?其實(shí)很簡(jiǎn)單,只需要在route后面拼接上參數(shù)就可以了,以創(chuàng)建FlutterView的方式為例。
View flutterView = Flutter.createView(this, getLifecycle(),
"route1?{\"name\":\"StephenCurry\"}");
這里將路由名稱和參數(shù)間用“?”隔開(kāi),就像瀏覽器中的url一樣,參數(shù)使用了Json格式傳遞,原因就是方便Flutter端解析,而且對(duì)于一些復(fù)雜的數(shù)據(jù),比如自定義對(duì)象,使用Json序列化也很好實(shí)現(xiàn)。這時(shí)候Flutter端通過(guò)window.defaultRouteName
獲取到的就是路由名稱+參數(shù)了,我們需要將路由名稱和參數(shù)分開(kāi),這就只是單純的字符串處理了,代碼如下所示:
String url = window.defaultRouteName;
// route名稱
String route =
url.indexOf('?') == -1 ? url : url.substring(0, url.indexOf('?'));
// 參數(shù)Json字符串
String paramsJson =
url.indexOf('?') == -1 ? '{}' : url.substring(url.indexOf('?') + 1);
// 解析參數(shù)
Map<String, dynamic> params = json.decode(paramsJson);
通過(guò)"?"將路由名稱和參數(shù)分開(kāi),將參數(shù)對(duì)應(yīng)的Json字符串解析為Map對(duì)象,需要導(dǎo)入dart:convert
包,之后再將參數(shù)傳遞給對(duì)應(yīng)的Widget即可,這里就不展示了,詳細(xì)代碼可以查看Demo。運(yùn)行效果如下圖所示:
Flutter頁(yè)面跳轉(zhuǎn)Android原生頁(yè)面
在實(shí)現(xiàn)Flutter頁(yè)面跳轉(zhuǎn)Android原生頁(yè)面之前首先介紹一下Platform Channel,它是Flutter和原生通信的工具,有三種類型:
- BasicMessageChannel:用于傳遞字符串和半結(jié)構(gòu)化的信息,F(xiàn)lutter和平臺(tái)端進(jìn)行消息數(shù)據(jù)交換時(shí)候可以使用。
- MethodChannel:用于傳遞方法調(diào)用(method invocation),F(xiàn)lutter和平臺(tái)端進(jìn)行直接方法調(diào)用時(shí)候可以使用。
- EventChannel:用于數(shù)據(jù)流(event streams)的通信,F(xiàn)lutter和平臺(tái)端進(jìn)行事件監(jiān)聽(tīng)、取消等可以使用。
這里我就只介紹一下MethodChannel的使用,它也是我們開(kāi)發(fā)中最常用的,關(guān)于其他兩種Channel的使用可以自行查閱網(wǎng)上的文章。Flutter跳轉(zhuǎn)原生頁(yè)面就是通過(guò)MethodChannel來(lái)實(shí)現(xiàn)的,在Flutter中調(diào)用原生的跳轉(zhuǎn)方法就可以了,接下來(lái)我們具體看一下如何實(shí)現(xiàn):
1.Android端
// 定義Channel名稱
private static final String CHANNEL_NATIVE = "com.example.flutter/native";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flutter_page);
// 通過(guò)FlutterView引入Flutter編寫的頁(yè)面
FlutterView flutterView = Flutter.createView(this, getLifecycle(),
"route1?{\"name\":\"" + getIntent().getStringExtra("name") + "\"}");
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
addContentView(flutterView, layout);
MethodChannel nativeChannel = new MethodChannel(flutterView, CHANNEL_NATIVE);
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
case "jumpToNative":
// 跳轉(zhuǎn)原生頁(yè)面
Intent jumpToNativeIntent = new Intent(FlutterPageActivity.this, NativePageActivity.class);
jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
startActivity(jumpToNativeIntent);
break;
default:
result.notImplemented();
break;
}
}
});
}
首先定義Channel名稱,需要保證是唯一的,在Flutter端需要使用同樣的名稱來(lái)創(chuàng)建MethodChannel。MethodChannel的構(gòu)造方法有三個(gè)參數(shù),第一個(gè)是messenger
,類型是BinaryMessenger,是一個(gè)接口,代表消息信使,是消息發(fā)送與接收的工具,由于FlutterView實(shí)現(xiàn)了BinaryMessenger,因此這里直接傳入了Flutter.createView()
方法的返回值;第二個(gè)參數(shù)是name
,就是Channel名稱;第三個(gè)參數(shù)是codec
,類型是MethodCodec,代表消息的編解碼器,這里沒(méi)有傳該參數(shù),默認(rèn)使用StandardMethodCodec。
這里補(bǔ)充一下,如果采用FlutterFragment的方式該如何獲取到FlutterView呢,我們可以查看一下FlutterFragment的源碼。
public class FlutterFragment extends Fragment {
public static final String ARG_ROUTE = "route";
private String mRoute = "/";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
mRoute = getArguments().getString(ARG_ROUTE);
}
}
@Override
public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
super.onInflate(context, attrs, savedInstanceState);
}
@Override
public FlutterView onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return Flutter.createView(getActivity(), getLifecycle(), mRoute);
}
}
可以看到FlutterFragment的onCreateView()
方法也是通過(guò)Flutter.createView()
創(chuàng)建了FlutterView并返回,因此可以通過(guò)Fragment的getView()
方法獲取到FlutterView。但是這里還有一個(gè)問(wèn)題,在Activity中通過(guò)Flutter.createFragment()
創(chuàng)建出Fragment后再調(diào)用getView()
方法獲取到的View為null,這是因?yàn)橹挥性?code>onCreateView()方法執(zhí)行完成后才會(huì)給Fragment持有的View賦值,關(guān)于這個(gè)問(wèn)題,我也沒(méi)有太好的解決方案,能想到的只是仿照FlutterFragment自定義一個(gè)Fragment,在內(nèi)部創(chuàng)建MethodChannel。
public class MyFlutterFragment extends FlutterFragment {
private static final String CHANNEL_NATIVE = "com.example.flutter/native";
public static MyFlutterFragment newInstance(String route) {
MyFlutterFragment fragment = new MyFlutterFragment();
Bundle args = new Bundle();
args.putString(ARG_ROUTE, route);
fragment.setArguments(args);
return fragment;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 這里保證了getView()返回值不為null
MethodChannel nativeChannel = new MethodChannel((FlutterView) getView(), CHANNEL_NATIVE);
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
case "jumpToNative":
// 跳轉(zhuǎn)原生頁(yè)面
Intent jumpToNativeIntent = new Intent(getActivity(), NativePageActivity.class);
jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
startActivity(jumpToNativeIntent);
break;
default:
result.notImplemented();
break;
}
}
});
}
}
創(chuàng)建FlutterFragment時(shí)使用MyFlutterFragment.newInstance()
代替Flutter.createFragment()
,傳入路由名稱和參數(shù)。
FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
MyFlutterFragment flutterFragment = MyFlutterFragment.newInstance("route1?{\"name\":\"StephenCurry\"}");
tx.replace(R.id.fl_container, flutterFragment);
tx.commit();
這樣就解決了FlutterFragment獲取FlutterView的問(wèn)題,不過(guò)我覺(jué)得這種方案并不好,將MethodChannel定義在了Fragment中,耦合度太高,如果大家有更好的解決方案歡迎提出,目前來(lái)看我還是建議使用Flutter.createView()
的方式來(lái)引入Flutter頁(yè)面。
回到正題,定義好了MethodChannel之后調(diào)用setMethodCallHandler()
方法設(shè)置消息處理回調(diào),參數(shù)是MethodHandler類型,需要實(shí)現(xiàn)它的onMethodCall()
方法。onMethodCall()
方法有兩個(gè)參數(shù)methodCall
和result
,methodCall
記錄了調(diào)用的方法信息,包括方法名和參數(shù),result
用于方法的返回值,可以通過(guò)result.success()
方法返回信息給Flutter端。之后根據(jù)方法名和參數(shù)來(lái)執(zhí)行原生的代碼就可以了,這里是跳轉(zhuǎn)到原生Activity。
2.Flutter端
在Flutter端同樣需要定義一個(gè)MethodChannel,使用MethodChannel需要引入services.dart
包,Channel名稱要和Android端定義的相同。
static const nativeChannel =
const MethodChannel('com.example.flutter/native');
在Flutter頁(yè)面中添加一個(gè)按鈕,點(diǎn)擊按鈕執(zhí)行跳轉(zhuǎn)原生頁(yè)面操作,通過(guò)調(diào)用MethodChannel的invokeMethod()
方法可以執(zhí)行原生代碼,該方法有兩個(gè)參數(shù),第一個(gè)是方法名,在Android端可以通過(guò)回調(diào)方法中的methodCall.method
獲取到;第二個(gè)是方法的參數(shù),可以不傳,在Android端可以通過(guò)methodCall.arguments()
以及methodCall.argument()
獲取到所有參數(shù)或者指定名稱的參數(shù)。
RaisedButton(
child: Text('跳轉(zhuǎn)Android原生頁(yè)面'),
onPressed: () {
// 跳轉(zhuǎn)原生頁(yè)面
Map<String, dynamic> result = {'name': 'KlayThompson'};
nativeChannel.invokeMethod('jumpToNative', result);
})
這里我們也注意到了,F(xiàn)lutter頁(yè)面跳轉(zhuǎn)原生頁(yè)面?zhèn)鬟f參數(shù)是通過(guò)invokeMethod()
方法的第二個(gè)參數(shù)實(shí)現(xiàn)的,在Android端通過(guò)methodCall.argument()
方法獲取到參數(shù)后再put到Intent里面就可以了。運(yùn)行效果如下圖所示:
到這里我們已經(jīng)基本實(shí)現(xiàn)了Flutter和Android原生之間的頁(yè)面跳轉(zhuǎn)和參數(shù)傳遞,此外還有一些需要我們注意的地方。
- 1.onActivityResult如何實(shí)現(xiàn)
在開(kāi)發(fā)中我們經(jīng)常會(huì)遇到關(guān)閉當(dāng)前頁(yè)面的同時(shí)返回給上一個(gè)頁(yè)面數(shù)據(jù)的場(chǎng)景,在Android中是通過(guò)startActivityForResult
和onActivityResult()
實(shí)現(xiàn)的,而純Flutter頁(yè)面之間可以通過(guò)在Navigator.of(context).pop()
方法中添加參數(shù)來(lái)實(shí)現(xiàn),那么對(duì)于Flutter頁(yè)面和Android原生頁(yè)面之間如何在返回上一頁(yè)時(shí)傳遞數(shù)據(jù)呢,通過(guò)MethodChannel就可以實(shí)現(xiàn)。
Flutter頁(yè)面返回Android原生頁(yè)面
這種情況直接在Flutter端調(diào)用原生的返回方法就可以了,首先在Flutter頁(yè)面添加一個(gè)按鈕,點(diǎn)擊按鈕返回原生頁(yè)面,代碼如下:
RaisedButton(
child: Text('返回上一頁(yè)'),
onPressed: () {
// 返回給上一頁(yè)的數(shù)據(jù)
Map<String, dynamic> result = {'message': '我從Flutter頁(yè)面回來(lái)了'};
nativeChannel.invokeMethod('goBackWithResult', result);
}),
Android端依然是通過(guò)判斷methodCall.method
的值來(lái)執(zhí)行指定的代碼,通過(guò)methodCall.argument()
獲取Flutter傳遞的參數(shù)。
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
case "goBackWithResult":
// 返回上一頁(yè),攜帶數(shù)據(jù)
Intent backIntent = new Intent();
backIntent.putExtra("message", (String) methodCall.argument("message"));
setResult(RESULT_OK, backIntent);
finish();
break;
}
}
});
之后在上一個(gè)Activity的onActivityResult()
方法中編寫邏輯就可以了,這里就不展示了。
Android原生頁(yè)面返回Flutter頁(yè)面
與上一種情況不同的是,這種情況需要原生來(lái)調(diào)用Flutter代碼,和Flutter調(diào)用原生方法的步驟是一樣的,我們來(lái)具體看一下。首先在Flutter跳轉(zhuǎn)到的頁(yè)面NativePageActivity中添加一個(gè)按鈕,點(diǎn)擊按鈕返回Flutter頁(yè)面,并傳遞數(shù)據(jù)。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_native_page);
Button btnBack = findViewById(R.id.btn_back);
btnBack.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.putExtra("message", "我從原生頁(yè)面回來(lái)了");
setResult(RESULT_OK, intent);
finish();
}
});
}
然后修改一下Flutter跳轉(zhuǎn)原生頁(yè)面的代碼,將startActivity
改為startActivityForResult
,并重寫onActivityResult()
方法,在方法內(nèi)部獲取到原生頁(yè)面返回的數(shù)據(jù),創(chuàng)建MethodChannel,調(diào)用invokeMethod()
方法將數(shù)據(jù)傳遞給Flutter端,這里定義的方法名為"onActivityResult"。
private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case 0:
if (data != null) {
// NativePageActivity返回的數(shù)據(jù)
String message = data.getStringExtra("message");
Map<String, Object> result = new HashMap<>();
result.put("message", message);
// 創(chuàng)建MethodChannel,這里的flutterView即Flutter.createView所返回的View
MethodChannel flutterChannel = new MethodChannel(flutterView, CHANNEL_FLUTTER);
// 調(diào)用Flutter端定義的方法
flutterChannel.invokeMethod("onActivityResult", result);
}
break;
default:
break;
}
}
接下來(lái)需要在Flutter端定義MethodChannel和回調(diào)方法,同樣是根據(jù)MethodCall.method
的值來(lái)執(zhí)行相應(yīng)代碼,通過(guò)MethodCall.arguments
來(lái)獲取參數(shù)。
static const flutterChannel =
const MethodChannel('com.example.flutter/flutter');
@override
void initState() {
super.initState();
Future<dynamic> handler(MethodCall call) async {
switch (call.method) {
case 'onActivityResult':
// 獲取原生頁(yè)面?zhèn)鬟f的參數(shù)
print(call.arguments['message']);
break;
}
}
flutterChannel.setMethodCallHandler(handler);
}
這樣就實(shí)現(xiàn)了原生頁(yè)面返回Flutter頁(yè)面并返回?cái)?shù)據(jù)的場(chǎng)景,獲取到數(shù)據(jù)后就可以為所欲為啦。
- 2.Flutter棧管理
看到這里不知道大家是否和我有相同的感受,在原生頁(yè)面(Activity)中引入Flutter頁(yè)面有些類似于Android開(kāi)發(fā)中使用WebView加載url,每個(gè)Flutter頁(yè)面對(duì)應(yīng)著一個(gè)route(url),那么我們自然就會(huì)想到一個(gè)問(wèn)題:如果在Flutter頁(yè)面中繼續(xù)跳轉(zhuǎn)到其他Flutter頁(yè)面,這時(shí)候點(diǎn)擊手機(jī)的返回鍵是否會(huì)直接返回到上一個(gè)Activity,而不是返回上一個(gè)Flutter頁(yè)面呢,通過(guò)測(cè)試發(fā)現(xiàn)確實(shí)是這樣。
那么應(yīng)該如何解決這個(gè)問(wèn)題呢,我的實(shí)現(xiàn)思路是在Flutter端利用Navigator.canPop(context)
方法判斷是否可以返回上一頁(yè),如果可以就調(diào)用Navigator.of(context).pop()
返回,反之則說(shuō)明當(dāng)前顯示的Flutter頁(yè)面已經(jīng)是第一個(gè)頁(yè)面了,直接返回上一個(gè)Activity即可。至于如何返回上一個(gè)Activity,當(dāng)然還是要使用MethodChannel了。既然明確了思路,我們就來(lái)看看具體實(shí)現(xiàn)吧。
首先在Flutter頁(yè)面中添加一個(gè)按鈕,點(diǎn)擊按鈕跳轉(zhuǎn)到一個(gè)新的Flutter頁(yè)面,這里的SecondPage是我新建的一個(gè)頁(yè)面,可以隨意修改,重點(diǎn)不在頁(yè)面本身,就不展示出來(lái)了。
RaisedButton(
child: Text('跳轉(zhuǎn)Flutter頁(yè)面'),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
return SecondPage();
}));
}),
然后定義MethodChannel和MethodCallHandler回調(diào),這里的邏輯是是調(diào)用Navigator.canPop(context)
判斷是否可以返回上一頁(yè),如果可以就調(diào)用Flutter自身的返回上一頁(yè)方法,如果已經(jīng)是第一個(gè)Flutter頁(yè)面了就調(diào)用原生方法返回上一個(gè)Activity,即這里的nativeChannel.invokeMethod('goBack')
。
static const nativeChannel =
const MethodChannel('com.example.flutter/native');
static const flutterChannel =
const MethodChannel('com.example.flutter/flutter');
@override
void initState() {
super.initState();
Future<dynamic> handler(MethodCall call) async {
switch (call.method) {
case 'goBack':
// 返回上一頁(yè)
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
} else {
nativeChannel.invokeMethod('goBack');
}
break;
}
}
flutterChannel.setMethodCallHandler(handler);
}
接下來(lái)我們?cè)賮?lái)看Android端,首先需要重寫onBackPressed()
方法,將返回鍵的事件處理交給Flutter端。
private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";
@Override
public void onBackPressed() {
MethodChannel flutterChannel = new MethodChannel(flutterView, CHANNEL_FLUTTER);
flutterChannel.invokeMethod("goBack", null);
}
最后編寫原生端的MethodCallHandler回調(diào),如果當(dāng)前Flutter頁(yè)面是第一個(gè)時(shí)調(diào)用該方法直接finish掉Activity。
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
case "goBack":
// 返回上一頁(yè)
finish();
break;
default:
result.notImplemented();
break;
}
現(xiàn)在我們?cè)賮?lái)看看運(yùn)行效果,這樣就很舒服了。
Flutter升級(jí)到1.12后遇到的問(wèn)題
前些日子評(píng)論區(qū)里wangwhatlh同學(xué)反饋
遇到了程序包io.flutter.facade不存在問(wèn)題,起初我運(yùn)行了一下之前的項(xiàng)目,發(fā)現(xiàn)可以正常運(yùn)行,加上我自己有一段時(shí)間沒(méi)有用過(guò)Flutter了,也就沒(méi)太重視這個(gè)問(wèn)題。說(shuō)來(lái)也是慚愧,最近又陸續(xù)有多位小伙伴反饋了這個(gè)問(wèn)題,我才終于意識(shí)到這是一個(gè)普遍性問(wèn)題,簡(jiǎn)單查了一下了解到這個(gè)錯(cuò)誤是Flutter 1.12版本廢棄了io.flutter.facade包導(dǎo)致的,我自己更新了Flutter版本后重新運(yùn)行項(xiàng)目也遇到了這個(gè)問(wèn)題,所以要對(duì)此前遇到這個(gè)問(wèn)題的大家說(shuō)聲抱歉,確實(shí)是我沒(méi)有重視這個(gè)問(wèn)題,之后對(duì)于大家提出的問(wèn)題我一定盡快反饋。
好了,接下來(lái)就介紹一下解決方案吧,首先附上官方的一些相關(guān)說(shuō)明文檔,大家可以自行閱讀一下文檔,文檔中介紹的還是比較詳細(xì)的。
Upgrading pre 1.12 Android projects
Experimental: Add Flutter View
Add Flutter to existing app
下面進(jìn)入正題,簡(jiǎn)單介紹一下在Flutter 1.12版本中幾個(gè)需要修改的地方。
- 原生頁(yè)面中引入Flutter
上文在介紹Android原生頁(yè)面跳轉(zhuǎn)Flutter頁(yè)面時(shí)提到了兩種方案:FlutterView和FlutterFragment,我們來(lái)分別看一下現(xiàn)在應(yīng)該如何實(shí)現(xiàn)。
首先是通過(guò)FlutterView引入Flutter頁(yè)面,以前我們是通過(guò)io.flutter.facade包中Flutter類的createView()
方法創(chuàng)建出一個(gè)FlutterView,然后添加到Activity的布局中,但是由于io.flutter.facade包的廢棄,該方法已經(jīng)無(wú)法使用。官方的文檔有說(shuō)明目前不提供在View級(jí)別引入Flutter的便捷API,因此如果可能的話,我們應(yīng)該避免使用FlutterView,但是通過(guò)FlutterView引入Flutter頁(yè)面也是可行的,代碼如下:
// 通過(guò)FlutterView引入Flutter編寫的頁(yè)面
FlutterView flutterView = new FlutterView(this);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
FrameLayout flContainer = findViewById(R.id.fl_container);
flContainer.addView(flutterView, lp);
// 關(guān)鍵代碼,將Flutter頁(yè)面顯示到FlutterView中
flutterView.attachToFlutterEngine(flutterEngine);
需要注意,這里的FlutterView位于io.flutter.embedding.android包中,和此前我們所創(chuàng)建的FlutterView(位于io.flutter.view包中)是不一樣的。我們通過(guò)查看FlutterView的源碼可以發(fā)現(xiàn)它繼承自FrameLayout,因此像一個(gè)普通的View那樣添加就可以了。接下來(lái)的這一步很關(guān)鍵,調(diào)用FlutterView的attachToFlutterEngine()
方法,這個(gè)方法的作用就是將Flutter編寫的UI頁(yè)面顯示到FlutterView中,我們注意到這里傳入了一個(gè)flutterEngine參數(shù),它又是什么呢?flutterEngine的類型為FlutterEngine,字面意思就是Flutter引擎,它負(fù)責(zé)在Android端執(zhí)行Dart代碼,將Flutter編寫的UI顯示到FlutterView/FlutterActivity/FlutterFragment中。創(chuàng)建FlutterEngine的代碼如下:
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
這樣就創(chuàng)建好了一個(gè)FlutterEngine對(duì)象,默認(rèn)情況下FlutterEngine加載的路由名稱為"/",我們可以通過(guò)下面的代碼指定初始路由名稱:
flutterEngine.getNavigationChannel().setInitialRoute("route1");
至于傳參的情況沒(méi)有變化,直接在路由名稱后面拼接參數(shù)就可以了。當(dāng)然,F(xiàn)lutterView也可以直接在xml布局文件中添加,最后同樣需要調(diào)用attachToFlutterEngine()
方法將Flutter編寫的UI頁(yè)面顯示到FlutterView中,這里就不展示了。
補(bǔ)充一下,最近我將Flutter版本更新到了1.17,發(fā)現(xiàn)上述代碼運(yùn)行后FlutterView無(wú)法顯示,和官方提供的示例flutter_view進(jìn)行了對(duì)比,才發(fā)現(xiàn)缺少了下面的代碼:
@Override
protected void onResume() {
super.onResume();
flutterEngine.getLifecycleChannel().appIsResumed();
}
@Override
protected void onPause() {
super.onPause();
flutterEngine.getLifecycleChannel().appIsInactive();
}
@Override
protected void onStop() {
super.onStop();
flutterEngine.getLifecycleChannel().appIsPaused();
}
相信大家都能看出這和生命周期有關(guān),flutterEngine.getLifecycleChannel()
獲取到的是一個(gè)LifecycleChannel對(duì)象,類比于MethodChannel,作用大概就是將Flutter和原生端的生命周期相互聯(lián)系起來(lái)。這里分別在onResume()
、onPause()
和onStop()
方法中調(diào)用了LifecycleChannel的appIsResumed()
、appIsInactive()
和appIsPaused()
方法,作用就是同步Flutter端與原生端的生命周期。添加上述代碼后,F(xiàn)lutterView就可以正常顯示了。至于為什么在Flutter 1.17版本(也有可能是更早的版本)中需要添加上述代碼,我猜想可能是FlutterVIew的渲染機(jī)制有了一些變化,在接收到原生端對(duì)應(yīng)生命周期方法中發(fā)送的通知才會(huì)顯示,具體原理我也不是很清楚,如果有說(shuō)得不對(duì)的地方或是大家有了解這部分內(nèi)容的歡迎提出。
然后是通過(guò)FlutterFragment引入Flutter頁(yè)面,我們此前是通過(guò)Flutter.createFragment()
方法創(chuàng)建出FlutterFragment,現(xiàn)在同樣無(wú)法使用了。官方提供了三種創(chuàng)建FlutterFragment的方式,我們來(lái)分別看一下。
方式一、FlutterFragment.createDefault()
// 通過(guò)FlutterFragment引入Flutter編寫的頁(yè)面
FlutterFragment flutterFragment = FlutterFragment.createDefault();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fl_container, flutterFragment)
.commit();
通過(guò)FlutterFragment.createDefault()
創(chuàng)建出FlutterFragment,需要注意這里的FlutterFragment位于io.flutter.embedding.android包中,和我們此前使用的FlutterFragment不是同一個(gè)類。創(chuàng)建好之后就沒(méi)什么可說(shuō)的了,按照正常的Fragment添加就好。createDefault()
方法創(chuàng)建出的Fragment顯示的路由名稱為"/",如果我們需要指定其他路由名稱就不能使用這個(gè)方法了。
方式二、FlutterFragment.withNewEngine()
// 通過(guò)FlutterFragment引入Flutter編寫的頁(yè)面
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.initialRoute("route1")
.build();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fl_container, flutterFragment)
.commit();
通過(guò)FlutterFragment.withNewEngine()
獲取到NewEngineFragmentBuilder對(duì)象,使用建造者模式構(gòu)造出FlutterFragment對(duì)象,可以通過(guò)initialRoute()
方法指定初始路由名稱。同樣地,傳遞參數(shù)只需要在路由名稱后面進(jìn)行拼接。
方式三、FlutterFragment.withCachedEngine
// 創(chuàng)建可緩存的FlutterEngine對(duì)象
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getNavigationChannel().setInitialRoute("route1");
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);
// 通過(guò)FlutterFragment引入Flutter編寫的頁(yè)面
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
.build();
方式二使用的withNewEngine()
方法從名稱上也能看出每次都是創(chuàng)建一個(gè)新的FlutterEngine對(duì)象來(lái)顯示Flutter UI,但是從官方文檔中我們可以了解到每個(gè)FlutterEngine對(duì)象在顯示出Flutter UI之前是需要一個(gè)warm-up(不知道能不能翻譯為預(yù)熱)期的,這會(huì)導(dǎo)致屏幕呈現(xiàn)短暫的空白,解決方式就是預(yù)先創(chuàng)建并啟動(dòng)FlutterEngine,完成warm-up過(guò)程,然后將這個(gè)FlutterEngine緩存起來(lái),之后使用這個(gè)FlutterEngine來(lái)顯示出Flutter UI。上面的代碼中執(zhí)行的FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
就是將FlutterEngine緩存起來(lái),這里傳入的"my_engine_id"就相當(dāng)于緩存名稱。之后通過(guò)FlutterFragment.withCachedEngine()
方法來(lái)創(chuàng)建FlutterFragment,參數(shù)傳入上面的緩存名稱。需要注意,withCachedEngine()
方法返回的是一個(gè)CachedEngineFragmentBuilder對(duì)象,同樣是使用了建造者模式,但是它是沒(méi)有initialRoute()
方法的,如果我們要指定初始路由,需要在創(chuàng)建FlutterEngine對(duì)象時(shí)通過(guò)setInitialRoute()
方法來(lái)設(shè)置。
除此之外,F(xiàn)lutter 1.12中還提供了一種原生引入Flutter頁(yè)面方式——使用FlutterActivity,這里的FlutterActivity也是位于io.flutter.embedding.android包下的。下面我簡(jiǎn)單介紹一下如何通過(guò)FlutterActivity引入Flutter編寫的UI,大家也可以參考官網(wǎng)的介紹。
首先需要在AndroidManifest.xml文件中注冊(cè)FlutterActivity,代碼如下:
<activity
android:name="io.flutter.embedding.android.FlutterActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:theme="@style/AppTheme"
android:windowSoftInputMode="adjustResize" />
這里的theme可以替換為自己項(xiàng)目中定義的主題。注冊(cè)好FlutterActivity后,第二步就是直接啟動(dòng)這個(gè)Activity了,啟動(dòng)FlutterActivity有以下三種方式:
// 方式一、FutterActivity顯示的路由名稱為"/",不可設(shè)置
startActivity(
FlutterActivity.createDefaultIntent(this)
);
// 方式二、FutterActivity顯示的路由名稱可設(shè)置,每次都創(chuàng)建一個(gè)新的FlutterEngine對(duì)象
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("route1")
.build(this)
);
// 方式三、FutterActivity顯示的路由名稱可設(shè)置,使用緩存好的FlutterEngine對(duì)象
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(this)
);
是不是很熟悉,和上面介紹的創(chuàng)建FlutterFragment的三種方式是對(duì)應(yīng)的,這里我就不再介紹了。與通過(guò)FlutterView/FlutterFragment引入Flutter UI不同,這種方式不需要我們自己創(chuàng)建一個(gè)Activity,F(xiàn)lutterActivity顯示的Flutter路由是在創(chuàng)建Intent對(duì)象時(shí)指定的,優(yōu)點(diǎn)就是使用起來(lái)更簡(jiǎn)單,缺點(diǎn)就是不夠靈活,無(wú)法像FlutterView/FlutterFragment那樣只是作為原生頁(yè)面中的一部分展示,因此這種方式更適合整個(gè)頁(yè)面都是由Flutter編寫的場(chǎng)景。
在調(diào)試階段,跳轉(zhuǎn)FlutterActivity之后也會(huì)黑屏幾秒,同樣地,打了release包后就沒(méi)有問(wèn)題了。
- Android原生與Flutter交互
交互這塊的變化不大,還是使用MethodChannel來(lái)進(jìn)行方法的調(diào)用,但是在Android端創(chuàng)建MethodChannel時(shí)需要注意了,我們此前都是傳入io.flutter.view包下的FlutterView作為BinaryMessenger,現(xiàn)在肯定是無(wú)法獲取到該類對(duì)象了,那么這個(gè)參數(shù)應(yīng)該傳什么呢。通過(guò)查看繼承關(guān)系我們可以找到兩個(gè)相關(guān)的類:DartExecutor和DartMessenger。DartExecutor可以通過(guò)FlutterEngine的getDartExecutor()
方法獲得,而DartMessenger又可以通過(guò)DartExecutor的getBinaryMessenger()
方法獲得,因此我們可以這樣創(chuàng)建MethodChannel:
MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor(), "com.example.flutter/native");
// 或
MethodChannel nativeChannel = new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), "com.example.flutter/native");
至于這兩種方式使用哪個(gè),我目前是沒(méi)發(fā)現(xiàn)有什么表現(xiàn)上的區(qū)別,如果哪位大佬有了解過(guò)可以分享一下。
此外,我們還需要注意,這里在創(chuàng)建MethodChannel時(shí)傳入的FlutterEngine對(duì)象必須和我們此前創(chuàng)建好的FlutterView/FlutterFragment中使用的是同一個(gè)。拿FlutterFragment舉例,上面介紹了三種創(chuàng)建FlutterFragment的方式,第三種是使用已經(jīng)創(chuàng)建好的FlutterEngine對(duì)象,我們可以直接傳入這個(gè)FlutterEngine對(duì)象。但是前兩種方式都是會(huì)創(chuàng)建出一個(gè)新的FlutterEngine,我們?nèi)绾潍@取到FlutterEngine對(duì)象呢?FlutterFragment中定義了一個(gè)getFlutterEngine()
方法,從方法名來(lái)看大概就是獲取FlutterEngine對(duì)象。我嘗試過(guò)創(chuàng)建MethodChannel時(shí)傳入flutterFragment.getFlutterEngine().getDartExecutor()
,運(yùn)行后會(huì)直接拋出空指針異常,異常產(chǎn)生的位置在FlutterFragment的getFlutterEngine()
方法中:
@Nullable
public FlutterEngine getFlutterEngine() {
return delegate.getFlutterEngine();
}
錯(cuò)誤原因是這里的delegate為null,全局搜索一下,發(fā)現(xiàn)在FlutterFragment的onAttach()
方法中會(huì)對(duì)delegate賦值,也就是說(shuō)明此時(shí)沒(méi)有執(zhí)行onAttach()
方法。我猜測(cè)這就是由于上面提到過(guò)的FlutterEngine的warm-up機(jī)制,這是一個(gè)耗時(shí)過(guò)程,因此FlutterFragment并不會(huì)立刻執(zhí)行onAttach()
方法,導(dǎo)致我們?cè)贏ctivity的onCreate()
方法中直接使用FlutterFragment的getFlutterEngine()
方法會(huì)拋出異常。目前我也沒(méi)想到有什么解決方案,如果我們要利用FlutterFragment來(lái)進(jìn)行交互,還是只能使用withCachedEngine()
方法來(lái)創(chuàng)建FlutterFragment,在構(gòu)造MethodChannel時(shí)傳入創(chuàng)建好的FlutterEngine對(duì)象。
到這里Flutter 1.12中關(guān)于原生交互的幾個(gè)變更基本上就介紹得差不多了,相關(guān)代碼我也已經(jīng)更新。最后還是要感嘆一下Flutter的更新速度,才幾個(gè)月沒(méi)看就變化這么大,之后的版本可能還會(huì)修改,如果我了解到有什么變更會(huì)及時(shí)更新文章,大家有什么發(fā)現(xiàn)也歡迎提出。
關(guān)于AndroidX
現(xiàn)在越來(lái)越多的Android項(xiàng)目都使用了AndroidX庫(kù),之前寫這篇文章時(shí)由于還是使用的support庫(kù),因此這里簡(jiǎn)單介紹一下AndroidX的遷移。
關(guān)于Android原生項(xiàng)目的遷移我就不介紹了,相信大家接觸過(guò)Android開(kāi)發(fā)的都有了解過(guò),網(wǎng)上也有很多相關(guān)文章。在Flutter版本1.12.13之后,新建的Module默認(rèn)就是使用AndroidX庫(kù)的,那么如何把現(xiàn)有的Flutter Module遷移到AndroidX呢,其實(shí)很簡(jiǎn)單,在Flutter Module根目錄下的pubspec.yaml文件中添加下面的配置就可以了:
module:
androidX: true // Add this line.
添加之后在命令行執(zhí)行flutter clean
命令即可,執(zhí)行完成后我們打開(kāi).android(或android)目錄下的gradle.properties文件,會(huì)看到添加了如下配置,這就說(shuō)明Flutter Module已經(jīng)成功遷移到了AndroidX庫(kù)。
當(dāng)我們把Flutter Module和Android項(xiàng)目都遷移到AndroidX庫(kù)之后會(huì)發(fā)現(xiàn)代碼中有報(bào)錯(cuò):
可以看到是在調(diào)用FragmentTransaction的add()
方法時(shí)報(bào)的錯(cuò),報(bào)錯(cuò)的原因就是因?yàn)檫@里傳入的參數(shù)類型(flutterFragment類型)不正確,我們可以點(diǎn)開(kāi)FlutterFragment類看一下,發(fā)現(xiàn)在類文件中定義的FlutterFragment類還是繼承自support包下的Fragment,那么這里當(dāng)然就會(huì)報(bào)錯(cuò)了。先別慌,當(dāng)你嘗試運(yùn)行項(xiàng)目時(shí)會(huì)發(fā)現(xiàn)可以正常運(yùn)行,這是為什么呢?其實(shí)一開(kāi)始我也有寫疑惑,后來(lái)仔細(xì)想了想大概是android.enableJetifier=true這個(gè)配置的作用,上面也見(jiàn)到過(guò),是項(xiàng)目遷移到AndroidX庫(kù)后自動(dòng)添加的,大家可能知道它的作用是將項(xiàng)目中的第三方庫(kù)自動(dòng)遷移到AndroidX庫(kù),因此雖然我們從源碼中看到的FlutterFragment類還是使用的support包,但是gardle在編譯時(shí)已經(jīng)自動(dòng)將FlutterFragment類遷移到了AndroidX庫(kù),所以運(yùn)行時(shí)不會(huì)報(bào)錯(cuò)。
關(guān)于AndroidX庫(kù)遷移的具體內(nèi)容大家也可以查看官方文檔AndroidX Migration。
目前最新版本(1.18.0)的Flutter中,F(xiàn)lutterFragment已經(jīng)改為繼承自androidx包下的Fragment了,編輯器也不會(huì)報(bào)錯(cuò)了,不過(guò)具體是哪個(gè)版本修改的我就不清楚了,如果大家有知道的歡迎提出。
總結(jié)
本文介紹了Android項(xiàng)目中引入Flutter的方法以及簡(jiǎn)單交互場(chǎng)景的實(shí)現(xiàn)。
1.Android項(xiàng)目引入Flutter本質(zhì)上是將Flutter編寫的Widget嵌入到Activity中,類似于WebView,容器Activity相當(dāng)于WebView,route相當(dāng)于url,有兩種方式FlutterView和FlutterFragment。頁(yè)面間的跳轉(zhuǎn)和傳參可以借助MethodChannel來(lái)實(shí)現(xiàn)。
2.關(guān)于MethodChannel,它的作用是Flutter和原生方法的互相調(diào)用,使用時(shí)在兩端都要定義MethodChannel,通過(guò)相同的name聯(lián)系起來(lái),調(diào)用方使用invokeMethod()
,傳入方法名和參數(shù);被調(diào)用方定義MethodCallHandler回調(diào),根據(jù)方法名和方法參數(shù)執(zhí)行相應(yīng)的平臺(tái)代碼。
3.本文中所提到的一些方案可能并不是最好的,如果大家有自己的見(jiàn)解歡迎一起交流學(xué)習(xí)。此外,文中的一些代碼只展示了部分,Demo我已經(jīng)上傳到了github,大家如果需要的話可以查看。
4.最后提一下flutter_boost,這是閑魚團(tuán)隊(duì)開(kāi)源的一個(gè)Flutter混合開(kāi)發(fā)插件,我簡(jiǎn)單地嘗試了一下,還是挺好用的,在頁(yè)面跳轉(zhuǎn)和傳參方面都很方便,大家感興趣的話可以了解一下。
參考文章
Add Flutter to existing app
Flutter混編:在Android原生中混編Flutter
flutter接入現(xiàn)有的app詳細(xì)介紹