前言
目前Flutter可以說是非常火熱了缸匪,多次更新過后也越來越穩(wěn)定翁狐,受到了很多開發(fā)者的青睞。不過純Flutter開發(fā)還是存在一定成本和風險的凌蔬,尤其是對于規(guī)模稍大一些的項目露懒,可能更加適合的是將Flutter用于項目中的某一個模塊,因此我們有必要了解一下如何在原生項目中引入Flutter砂心。
本文介紹一下Android原生項目引入Flutter的方法以及Flutter如何與原生進行交互懈词,包括頁面間的跳轉(zhuǎn)和方法的調(diào)用,本人不懂IOS開發(fā)辩诞,有需要的話還是自行百度吧o(╥﹏╥)o坎弯,但是基本思路我覺得不會差太多的。
Android原生項目中引入Flutter
這應該是目前Flutter在實際開發(fā)中應用最多的一種場景译暂,在已有的Android原生項目中引入Flutter抠忘,針對一些復雜的頁面,使用Flutter開發(fā)可以有效地提高開發(fā)效率外永。
官方提供的文檔Add Flutter to existing apps詳細介紹了原生app引入Flutter的步驟崎脉,不過很遺憾是英文的。我也是參考了網(wǎng)上的一些相關(guān)文章伯顶,總結(jié)了一下文檔中的提到的幾個步驟囚灼。
- 第一步呛踊、新建Android項目
這沒什么可說的,畢竟我們是要在原生項目中引入Flutter嘛啦撮。
- 第二步谭网、新建Flutter Module
有兩種方式來創(chuàng)建Flutter Module,第一種是通過命令行來創(chuàng)建赃春,首先切換到Android項目的同級目錄下愉择,執(zhí)行以下命令:
flutter create -t module my_flutter
其中my_flutter為module的名字。第二種是直接使用Android Studio來創(chuàng)建织中,依次點擊左上角的File --> New --> New Flutter Project锥涕,然后選擇Flutter Module。
然后填寫module的名稱狭吼、路徑层坠。
最后填寫module的包名,點擊Finish就創(chuàng)建好了一個Flutter Module刁笙。
- 第三步破花、在Android項目中引入Flutter Module
首先在app下的build.gradle文件中添加以下配置:
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
我們知道這是使用Java 8所需要的配置,在這里的作用是為了解決版本兼容問題疲吸,如果不配置的話運行項目可能會報錯:Invoke-customs are only supported starting with Android O (--min-api 26)座每。
然后在項目根目錄下的setting.gradle文件中配置:
include ':app'
// 加入下面配置
setBinding(new Binding([gradle: this]))
evaluate(new File(
settingsDir.parentFile,
'my_flutter/.android/include_flutter.groovy'
))
記得修改成自己的Flutter Module名稱,之后Sync一下項目摘悴。Binding可能會因為找不到而標紅峭梳,我沒有導包最后也可以Sync成功,并不影響module的引入蹂喻,這一點我還不清楚是什么原因葱椭,如果有知道的小伙伴歡迎提出。
Sync后我們可以看到項目中多了一個名稱為flutter的library module口四,我們需要在app下的build.gradle文件中添加該module的依賴孵运。
implementation project(':flutter')
這樣就成功地將Flutter引入到了Android原生項目中。
Android和Flutter的交互
通過上面的幾個步驟我們已經(jīng)在Android原生項目中集成了Flutter窃祝,之后就需要解決交互問題了掐松。首先介紹一下Android頁面和Flutter頁面之間的跳轉(zhuǎn)踱侣。
Tips:由于Flutter版本的更新粪小,下面介紹的內(nèi)容中存在一些API已經(jīng)被廢棄的情況,不過各種交互場景的處理思路是不變的抡句,關(guān)于Flutter版本變更的內(nèi)容我補充到了文章最后探膊,大家可以結(jié)合起來看。
Android原生頁面跳轉(zhuǎn)Flutter頁面
基本思路就是將Flutter編寫的頁面嵌入到Activity中待榔,官方提供了兩種方式:通過FlutterView
和FlutterFragment
逞壁,下面我們分別看一下這兩種方式是如何實現(xiàn)的流济。
1.使用FlutterView
首先新建一個Activity,命名為FlutterPageActivity(名稱隨意起)腌闯,在onCreate()
方法中添加以下代碼:
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 通過FlutterView引入Flutter編寫的頁面
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()
方法返回的是一個FlutterView绳瘟,它繼承自View,我們可以把它當做一個普通的View姿骏,調(diào)用addContentView()
方法將這個View添加到Activity的contentView中糖声。我們注意到Flutter.createView()
方法的第三個參數(shù)傳入了"route1"字符串,表示路由名稱分瘦,它確定了Flutter中要顯示的Widget蘸泻,接下來需要在之前創(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頁面'),
),
body: Center(
child: Text('Flutter頁面嘲玫,route=$route'),
),
),
);
default:
return Center(
child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
);
}
}
在runApp()
方法中通過window.defaultRouteName
可以獲取到我們在Flutter.createView()
方法中傳入的路由名稱悦施,即"route1",之后編寫了一個_widgetForRoute()
方法去团,根據(jù)傳入的route字符串顯示相應的Widget抡诞。
最后在MainActivity中添加一個Button,編寫點擊事件土陪,點擊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);
}
});
運行項目,點擊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);
然后需要隱藏原生的標題欄璧疗,在資源文件夾res/values中的style.xml文件中添加一個FlutterPageTheme坯辩。
<style name="FlutterPageTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!--狀態(tài)欄透明-->
<item name="android:windowTranslucentStatus">true</item>
</style>
然后在AndroidManifest.xml文件中設置Activity的Theme穷吮。
<activity
android:name=".FlutterPageActivity"
android:theme="@style/FlutterPageTheme" />
再次運行項目看一下效果彤枢,這樣就自然多了骗奖,當然我們還可以繼續(xù)修改標題欄的背景顏色跟啤,這里就不提了管引。
2.使用FlutterFragment
為了簡單庶柿,我們依然使用FlutterPageActivity伟众,新建一個布局文件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);
// 通過FlutterFragment引入Flutter編寫的頁面
FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
tx.replace(R.id.fl_container, Flutter.createFragment("route1"));
tx.commit();
}
Flutter.createFragment()
方法傳入的參數(shù)同樣表示路由名稱斤葱,用于確定Flutter要顯示的Widget系瓢,返回一個FlutterFragment阿纤,該類繼承自Fragment,將該Fragment添加到Activity中就可以了夷陋。
在調(diào)試時會遇到一個問題欠拾,顯示出Flutter頁面之前會黑屏幾秒胰锌,不要擔心,打了release包后就沒問題了藐窄。
如何傳遞參數(shù)跳轉(zhuǎn)
通過以上兩種方式實現(xiàn)了將Flutter編寫的頁面嵌入到Activity中资昧,但是這只是最簡單的情況,如果我們需要在頁面跳轉(zhuǎn)時傳遞參數(shù)呢荆忍,如何在Flutter代碼中獲取到原生代碼中的參數(shù)呢榛搔?其實很簡單,只需要在route后面拼接上參數(shù)就可以了东揣,以創(chuàng)建FlutterView的方式為例践惑。
View flutterView = Flutter.createView(this, getLifecycle(),
"route1?{\"name\":\"StephenCurry\"}");
這里將路由名稱和參數(shù)間用“?”隔開嘶卧,就像瀏覽器中的url一樣尔觉,參數(shù)使用了Json格式傳遞,原因就是方便Flutter端解析芥吟,而且對于一些復雜的數(shù)據(jù)侦铜,比如自定義對象,使用Json序列化也很好實現(xiàn)钟鸵。這時候Flutter端通過window.defaultRouteName
獲取到的就是路由名稱+參數(shù)了钉稍,我們需要將路由名稱和參數(shù)分開,這就只是單純的字符串處理了棺耍,代碼如下所示:
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);
通過"?"將路由名稱和參數(shù)分開贡未,將參數(shù)對應的Json字符串解析為Map對象,需要導入dart:convert
包蒙袍,之后再將參數(shù)傳遞給對應的Widget即可俊卤,這里就不展示了,詳細代碼可以查看Demo害幅。運行效果如下圖所示:
Flutter頁面跳轉(zhuǎn)Android原生頁面
在實現(xiàn)Flutter頁面跳轉(zhuǎn)Android原生頁面之前首先介紹一下Platform Channel消恍,它是Flutter和原生通信的工具,有三種類型:
- BasicMessageChannel:用于傳遞字符串和半結(jié)構(gòu)化的信息以现,F(xiàn)lutter和平臺端進行消息數(shù)據(jù)交換時候可以使用狠怨。
- MethodChannel:用于傳遞方法調(diào)用(method invocation),F(xiàn)lutter和平臺端進行直接方法調(diào)用時候可以使用邑遏。
- EventChannel:用于數(shù)據(jù)流(event streams)的通信佣赖,F(xiàn)lutter和平臺端進行事件監(jiān)聽、取消等可以使用无宿。
這里我就只介紹一下MethodChannel的使用茵汰,它也是我們開發(fā)中最常用的枢里,關(guān)于其他兩種Channel的使用可以自行查閱網(wǎng)上的文章孽鸡。Flutter跳轉(zhuǎn)原生頁面就是通過MethodChannel來實現(xiàn)的蹂午,在Flutter中調(diào)用原生的跳轉(zhuǎn)方法就可以了,接下來我們具體看一下如何實現(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);
// 通過FlutterView引入Flutter編寫的頁面
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)原生頁面
Intent jumpToNativeIntent = new Intent(FlutterPageActivity.this, NativePageActivity.class);
jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
startActivity(jumpToNativeIntent);
break;
default:
result.notImplemented();
break;
}
}
});
}
首先定義Channel名稱彬碱,需要保證是唯一的豆胸,在Flutter端需要使用同樣的名稱來創(chuàng)建MethodChannel。MethodChannel的構(gòu)造方法有三個參數(shù)巷疼,第一個是messenger
晚胡,類型是BinaryMessenger,是一個接口嚼沿,代表消息信使估盘,是消息發(fā)送與接收的工具,由于FlutterView實現(xiàn)了BinaryMessenger骡尽,因此這里直接傳入了Flutter.createView()
方法的返回值遣妥;第二個參數(shù)是name
,就是Channel名稱攀细;第三個參數(shù)是codec
箫踩,類型是MethodCodec,代表消息的編解碼器谭贪,這里沒有傳該參數(shù)境钟,默認使用StandardMethodCodec。
這里補充一下俭识,如果采用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()
方法也是通過Flutter.createView()
創(chuàng)建了FlutterView并返回套媚,因此可以通過Fragment的getView()
方法獲取到FlutterView理盆。但是這里還有一個問題,在Activity中通過Flutter.createFragment()
創(chuàng)建出Fragment后再調(diào)用getView()
方法獲取到的View為null凑阶,這是因為只有在onCreateView()
方法執(zhí)行完成后才會給Fragment持有的View賦值猿规,關(guān)于這個問題,我也沒有太好的解決方案宙橱,能想到的只是仿照FlutterFragment自定義一個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)原生頁面
Intent jumpToNativeIntent = new Intent(getActivity(), NativePageActivity.class);
jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
startActivity(jumpToNativeIntent);
break;
default:
result.notImplemented();
break;
}
}
});
}
}
創(chuàng)建FlutterFragment時使用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的問題,不過我覺得這種方案并不好宝冕,將MethodChannel定義在了Fragment中张遭,耦合度太高,如果大家有更好的解決方案歡迎提出地梨,目前來看我還是建議使用Flutter.createView()
的方式來引入Flutter頁面菊卷。
回到正題缔恳,定義好了MethodChannel之后調(diào)用setMethodCallHandler()
方法設置消息處理回調(diào),參數(shù)是MethodHandler類型洁闰,需要實現(xiàn)它的onMethodCall()
方法歉甚。onMethodCall()
方法有兩個參數(shù)methodCall
和result
,methodCall
記錄了調(diào)用的方法信息扑眉,包括方法名和參數(shù)纸泄,result
用于方法的返回值,可以通過result.success()
方法返回信息給Flutter端腰素。之后根據(jù)方法名和參數(shù)來執(zhí)行原生的代碼就可以了聘裁,這里是跳轉(zhuǎn)到原生Activity。
2.Flutter端
在Flutter端同樣需要定義一個MethodChannel弓千,使用MethodChannel需要引入services.dart
包咧虎,Channel名稱要和Android端定義的相同。
static const nativeChannel =
const MethodChannel('com.example.flutter/native');
在Flutter頁面中添加一個按鈕计呈,點擊按鈕執(zhí)行跳轉(zhuǎn)原生頁面操作砰诵,通過調(diào)用MethodChannel的invokeMethod()
方法可以執(zhí)行原生代碼,該方法有兩個參數(shù)捌显,第一個是方法名茁彭,在Android端可以通過回調(diào)方法中的methodCall.method
獲取到;第二個是方法的參數(shù)扶歪,可以不傳理肺,在Android端可以通過methodCall.arguments()
以及methodCall.argument()
獲取到所有參數(shù)或者指定名稱的參數(shù)。
RaisedButton(
child: Text('跳轉(zhuǎn)Android原生頁面'),
onPressed: () {
// 跳轉(zhuǎn)原生頁面
Map<String, dynamic> result = {'name': 'KlayThompson'};
nativeChannel.invokeMethod('jumpToNative', result);
})
這里我們也注意到了善镰,F(xiàn)lutter頁面跳轉(zhuǎn)原生頁面?zhèn)鬟f參數(shù)是通過invokeMethod()
方法的第二個參數(shù)實現(xiàn)的妹萨,在Android端通過methodCall.argument()
方法獲取到參數(shù)后再put到Intent里面就可以了。運行效果如下圖所示:
到這里我們已經(jīng)基本實現(xiàn)了Flutter和Android原生之間的頁面跳轉(zhuǎn)和參數(shù)傳遞炫欺,此外還有一些需要我們注意的地方乎完。
- 1.onActivityResult如何實現(xiàn)
在開發(fā)中我們經(jīng)常會遇到關(guān)閉當前頁面的同時返回給上一個頁面數(shù)據(jù)的場景,在Android中是通過startActivityForResult
和onActivityResult()
實現(xiàn)的品洛,而純Flutter頁面之間可以通過在Navigator.of(context).pop()
方法中添加參數(shù)來實現(xiàn)树姨,那么對于Flutter頁面和Android原生頁面之間如何在返回上一頁時傳遞數(shù)據(jù)呢,通過MethodChannel就可以實現(xiàn)桥状。
Flutter頁面返回Android原生頁面
這種情況直接在Flutter端調(diào)用原生的返回方法就可以了帽揪,首先在Flutter頁面添加一個按鈕,點擊按鈕返回原生頁面辅斟,代碼如下:
RaisedButton(
child: Text('返回上一頁'),
onPressed: () {
// 返回給上一頁的數(shù)據(jù)
Map<String, dynamic> result = {'message': '我從Flutter頁面回來了'};
nativeChannel.invokeMethod('goBackWithResult', result);
}),
Android端依然是通過判斷methodCall.method
的值來執(zhí)行指定的代碼转晰,通過methodCall.argument()
獲取Flutter傳遞的參數(shù)。
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
case "goBackWithResult":
// 返回上一頁,攜帶數(shù)據(jù)
Intent backIntent = new Intent();
backIntent.putExtra("message", (String) methodCall.argument("message"));
setResult(RESULT_OK, backIntent);
finish();
break;
}
}
});
之后在上一個Activity的onActivityResult()
方法中編寫邏輯就可以了查邢,這里就不展示了蔗崎。
Android原生頁面返回Flutter頁面
與上一種情況不同的是,這種情況需要原生來調(diào)用Flutter代碼侠坎,和Flutter調(diào)用原生方法的步驟是一樣的蚁趁,我們來具體看一下裙盾。首先在Flutter跳轉(zhuǎn)到的頁面NativePageActivity中添加一個按鈕实胸,點擊按鈕返回Flutter頁面,并傳遞數(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", "我從原生頁面回來了");
setResult(RESULT_OK, intent);
finish();
}
});
}
然后修改一下Flutter跳轉(zhuǎn)原生頁面的代碼庐完,將startActivity
改為startActivityForResult
,并重寫onActivityResult()
方法徘熔,在方法內(nèi)部獲取到原生頁面返回的數(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;
}
}
接下來需要在Flutter端定義MethodChannel和回調(diào)方法山孔,同樣是根據(jù)MethodCall.method
的值來執(zhí)行相應代碼懂讯,通過MethodCall.arguments
來獲取參數(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':
// 獲取原生頁面?zhèn)鬟f的參數(shù)
print(call.arguments['message']);
break;
}
}
flutterChannel.setMethodCallHandler(handler);
}
這樣就實現(xiàn)了原生頁面返回Flutter頁面并返回數(shù)據(jù)的場景台颠,獲取到數(shù)據(jù)后就可以為所欲為啦褐望。
- 2.Flutter棧管理
看到這里不知道大家是否和我有相同的感受,在原生頁面(Activity)中引入Flutter頁面有些類似于Android開發(fā)中使用WebView加載url串前,每個Flutter頁面對應著一個route(url)瘫里,那么我們自然就會想到一個問題:如果在Flutter頁面中繼續(xù)跳轉(zhuǎn)到其他Flutter頁面,這時候點擊手機的返回鍵是否會直接返回到上一個Activity荡碾,而不是返回上一個Flutter頁面呢谨读,通過測試發(fā)現(xiàn)確實是這樣。
那么應該如何解決這個問題呢坛吁,我的實現(xiàn)思路是在Flutter端利用Navigator.canPop(context)
方法判斷是否可以返回上一頁漆腌,如果可以就調(diào)用Navigator.of(context).pop()
返回,反之則說明當前顯示的Flutter頁面已經(jīng)是第一個頁面了阶冈,直接返回上一個Activity即可闷尿。至于如何返回上一個Activity,當然還是要使用MethodChannel了女坑。既然明確了思路填具,我們就來看看具體實現(xiàn)吧。
首先在Flutter頁面中添加一個按鈕,點擊按鈕跳轉(zhuǎn)到一個新的Flutter頁面劳景,這里的SecondPage是我新建的一個頁面誉简,可以隨意修改,重點不在頁面本身盟广,就不展示出來了闷串。
RaisedButton(
child: Text('跳轉(zhuǎn)Flutter頁面'),
onPressed: () {
Navigator.of(context)
.push(MaterialPageRoute(builder: (context) {
return SecondPage();
}));
}),
然后定義MethodChannel和MethodCallHandler回調(diào),這里的邏輯是是調(diào)用Navigator.canPop(context)
判斷是否可以返回上一頁筋量,如果可以就調(diào)用Flutter自身的返回上一頁方法烹吵,如果已經(jīng)是第一個Flutter頁面了就調(diào)用原生方法返回上一個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':
// 返回上一頁
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
} else {
nativeChannel.invokeMethod('goBack');
}
break;
}
}
flutterChannel.setMethodCallHandler(handler);
}
接下來我們再來看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)凉蜂,如果當前Flutter頁面是第一個時調(diào)用該方法直接finish掉Activity。
nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method) {
case "goBack":
// 返回上一頁
finish();
break;
default:
result.notImplemented();
break;
}
現(xiàn)在我們再來看看運行效果性誉,這樣就很舒服了窿吩。
Flutter升級到1.12后遇到的問題
前些日子評論區(qū)里wangwhatlh同學反饋
遇到了程序包io.flutter.facade不存在問題,起初我運行了一下之前的項目错览,發(fā)現(xiàn)可以正常運行纫雁,加上我自己有一段時間沒有用過Flutter了,也就沒太重視這個問題蝗砾。說來也是慚愧先较,最近又陸續(xù)有多位小伙伴反饋了這個問題,我才終于意識到這是一個普遍性問題悼粮,簡單查了一下了解到這個錯誤是Flutter 1.12版本廢棄了io.flutter.facade包導致的闲勺,我自己更新了Flutter版本后重新運行項目也遇到了這個問題,所以要對此前遇到這個問題的大家說聲抱歉扣猫,確實是我沒有重視這個問題菜循,之后對于大家提出的問題我一定盡快反饋。
好了申尤,接下來就介紹一下解決方案吧癌幕,首先附上官方的一些相關(guān)說明文檔,大家可以自行閱讀一下文檔昧穿,文檔中介紹的還是比較詳細的勺远。
Upgrading pre 1.12 Android projects
Experimental: Add Flutter View
Add Flutter to existing app
下面進入正題,簡單介紹一下在Flutter 1.12版本中幾個需要修改的地方时鸵。
- 原生頁面中引入Flutter
上文在介紹Android原生頁面跳轉(zhuǎn)Flutter頁面時提到了兩種方案:FlutterView和FlutterFragment胶逢,我們來分別看一下現(xiàn)在應該如何實現(xiàn)厅瞎。
首先是通過FlutterView引入Flutter頁面,以前我們是通過io.flutter.facade包中Flutter類的createView()
方法創(chuàng)建出一個FlutterView初坠,然后添加到Activity的布局中和簸,但是由于io.flutter.facade包的廢棄,該方法已經(jīng)無法使用碟刺。官方的文檔有說明目前不提供在View級別引入Flutter的便捷API锁保,因此如果可能的話,我們應該避免使用FlutterView半沽,但是通過FlutterView引入Flutter頁面也是可行的爽柒,代碼如下:
// 通過FlutterView引入Flutter編寫的頁面
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頁面顯示到FlutterView中
flutterView.attachToFlutterEngine(flutterEngine);
需要注意抄囚,這里的FlutterView位于io.flutter.embedding.android包中霉赡,和此前我們所創(chuàng)建的FlutterView(位于io.flutter.view包中)是不一樣的橄务。我們通過查看FlutterView的源碼可以發(fā)現(xiàn)它繼承自FrameLayout幔托,因此像一個普通的View那樣添加就可以了。接下來的這一步很關(guān)鍵蜂挪,調(diào)用FlutterView的attachToFlutterEngine()
方法重挑,這個方法的作用就是將Flutter編寫的UI頁面顯示到FlutterView中,我們注意到這里傳入了一個flutterEngine參數(shù)棠涮,它又是什么呢谬哀?flutterEngine的類型為FlutterEngine,字面意思就是Flutter引擎严肪,它負責在Android端執(zhí)行Dart代碼史煎,將Flutter編寫的UI顯示到FlutterView/FlutterActivity/FlutterFragment中。創(chuàng)建FlutterEngine的代碼如下:
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
這樣就創(chuàng)建好了一個FlutterEngine對象驳糯,默認情況下FlutterEngine加載的路由名稱為"/"篇梭,我們可以通過下面的代碼指定初始路由名稱:
flutterEngine.getNavigationChannel().setInitialRoute("route1");
至于傳參的情況沒有變化,直接在路由名稱后面拼接參數(shù)就可以了酝枢。當然恬偷,F(xiàn)lutterView也可以直接在xml布局文件中添加,最后同樣需要調(diào)用attachToFlutterEngine()
方法將Flutter編寫的UI頁面顯示到FlutterView中帘睦,這里就不展示了袍患。
補充一下,最近我將Flutter版本更新到了1.17竣付,發(fā)現(xiàn)上述代碼運行后FlutterView無法顯示诡延,和官方提供的示例flutter_view進行了對比,才發(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()
獲取到的是一個LifecycleChannel對象肆良,類比于MethodChannel,作用大概就是將Flutter和原生端的生命周期相互聯(lián)系起來。這里分別在onResume()
妖滔、onPause()
和onStop()
方法中調(diào)用了LifecycleChannel的appIsResumed()
隧哮、appIsInactive()
和appIsPaused()
方法,作用就是同步Flutter端與原生端的生命周期座舍。添加上述代碼后沮翔,F(xiàn)lutterView就可以正常顯示了。至于為什么在Flutter 1.17版本(也有可能是更早的版本)中需要添加上述代碼曲秉,我猜想可能是FlutterVIew的渲染機制有了一些變化采蚀,在接收到原生端對應生命周期方法中發(fā)送的通知才會顯示,具體原理我也不是很清楚承二,如果有說得不對的地方或是大家有了解這部分內(nèi)容的歡迎提出榆鼠。
然后是通過FlutterFragment引入Flutter頁面,我們此前是通過Flutter.createFragment()
方法創(chuàng)建出FlutterFragment亥鸠,現(xiàn)在同樣無法使用了妆够。官方提供了三種創(chuàng)建FlutterFragment的方式,我們來分別看一下负蚊。
方式一神妹、FlutterFragment.createDefault()
// 通過FlutterFragment引入Flutter編寫的頁面
FlutterFragment flutterFragment = FlutterFragment.createDefault();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fl_container, flutterFragment)
.commit();
通過FlutterFragment.createDefault()
創(chuàng)建出FlutterFragment,需要注意這里的FlutterFragment位于io.flutter.embedding.android包中家妆,和我們此前使用的FlutterFragment不是同一個類鸵荠。創(chuàng)建好之后就沒什么可說的了,按照正常的Fragment添加就好伤极。createDefault()
方法創(chuàng)建出的Fragment顯示的路由名稱為"/"蛹找,如果我們需要指定其他路由名稱就不能使用這個方法了。
方式二哨坪、FlutterFragment.withNewEngine()
// 通過FlutterFragment引入Flutter編寫的頁面
FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
.initialRoute("route1")
.build();
getSupportFragmentManager()
.beginTransaction()
.add(R.id.fl_container, flutterFragment)
.commit();
通過FlutterFragment.withNewEngine()
獲取到NewEngineFragmentBuilder對象庸疾,使用建造者模式構(gòu)造出FlutterFragment對象,可以通過initialRoute()
方法指定初始路由名稱齿税。同樣地彼硫,傳遞參數(shù)只需要在路由名稱后面進行拼接。
方式三凌箕、FlutterFragment.withCachedEngine
// 創(chuàng)建可緩存的FlutterEngine對象
FlutterEngine flutterEngine = new FlutterEngine(this);
flutterEngine.getNavigationChannel().setInitialRoute("route1");
flutterEngine.getDartExecutor().executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
);
FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);
// 通過FlutterFragment引入Flutter編寫的頁面
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id")
.build();
方式二使用的withNewEngine()
方法從名稱上也能看出每次都是創(chuàng)建一個新的FlutterEngine對象來顯示Flutter UI拧篮,但是從官方文檔中我們可以了解到每個FlutterEngine對象在顯示出Flutter UI之前是需要一個warm-up(不知道能不能翻譯為預熱)期的,這會導致屏幕呈現(xiàn)短暫的空白牵舱,解決方式就是預先創(chuàng)建并啟動FlutterEngine串绩,完成warm-up過程,然后將這個FlutterEngine緩存起來芜壁,之后使用這個FlutterEngine來顯示出Flutter UI礁凡。上面的代碼中執(zhí)行的FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine)
就是將FlutterEngine緩存起來高氮,這里傳入的"my_engine_id"就相當于緩存名稱。之后通過FlutterFragment.withCachedEngine()
方法來創(chuàng)建FlutterFragment顷牌,參數(shù)傳入上面的緩存名稱剪芍。需要注意,withCachedEngine()
方法返回的是一個CachedEngineFragmentBuilder對象窟蓝,同樣是使用了建造者模式罪裹,但是它是沒有initialRoute()
方法的,如果我們要指定初始路由运挫,需要在創(chuàng)建FlutterEngine對象時通過setInitialRoute()
方法來設置状共。
除此之外,F(xiàn)lutter 1.12中還提供了一種原生引入Flutter頁面方式——使用FlutterActivity谁帕,這里的FlutterActivity也是位于io.flutter.embedding.android包下的峡继。下面我簡單介紹一下如何通過FlutterActivity引入Flutter編寫的UI,大家也可以參考官網(wǎng)的介紹匈挖。
首先需要在AndroidManifest.xml文件中注冊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可以替換為自己項目中定義的主題。注冊好FlutterActivity后关划,第二步就是直接啟動這個Activity了小染,啟動FlutterActivity有以下三種方式:
// 方式一翘瓮、FutterActivity顯示的路由名稱為"/"贮折,不可設置
startActivity(
FlutterActivity.createDefaultIntent(this)
);
// 方式二、FutterActivity顯示的路由名稱可設置资盅,每次都創(chuàng)建一個新的FlutterEngine對象
startActivity(
FlutterActivity
.withNewEngine()
.initialRoute("route1")
.build(this)
);
// 方式三调榄、FutterActivity顯示的路由名稱可設置,使用緩存好的FlutterEngine對象
startActivity(
FlutterActivity
.withCachedEngine("my_engine_id")
.build(this)
);
是不是很熟悉呵扛,和上面介紹的創(chuàng)建FlutterFragment的三種方式是對應的每庆,這里我就不再介紹了。與通過FlutterView/FlutterFragment引入Flutter UI不同今穿,這種方式不需要我們自己創(chuàng)建一個Activity缤灵,F(xiàn)lutterActivity顯示的Flutter路由是在創(chuàng)建Intent對象時指定的,優(yōu)點就是使用起來更簡單蓝晒,缺點就是不夠靈活腮出,無法像FlutterView/FlutterFragment那樣只是作為原生頁面中的一部分展示,因此這種方式更適合整個頁面都是由Flutter編寫的場景芝薇。
在調(diào)試階段胚嘲,跳轉(zhuǎn)FlutterActivity之后也會黑屏幾秒,同樣地洛二,打了release包后就沒有問題了馋劈。
- Android原生與Flutter交互
交互這塊的變化不大攻锰,還是使用MethodChannel來進行方法的調(diào)用,但是在Android端創(chuàng)建MethodChannel時需要注意了妓雾,我們此前都是傳入io.flutter.view包下的FlutterView作為BinaryMessenger娶吞,現(xiàn)在肯定是無法獲取到該類對象了,那么這個參數(shù)應該傳什么呢械姻。通過查看繼承關(guān)系我們可以找到兩個相關(guān)的類:DartExecutor和DartMessenger寝志。DartExecutor可以通過FlutterEngine的getDartExecutor()
方法獲得,而DartMessenger又可以通過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");
至于這兩種方式使用哪個材部,我目前是沒發(fā)現(xiàn)有什么表現(xiàn)上的區(qū)別,如果哪位大佬有了解過可以分享一下唯竹。
此外乐导,我們還需要注意,這里在創(chuàng)建MethodChannel時傳入的FlutterEngine對象必須和我們此前創(chuàng)建好的FlutterView/FlutterFragment中使用的是同一個浸颓。拿FlutterFragment舉例物臂,上面介紹了三種創(chuàng)建FlutterFragment的方式,第三種是使用已經(jīng)創(chuàng)建好的FlutterEngine對象产上,我們可以直接傳入這個FlutterEngine對象棵磷。但是前兩種方式都是會創(chuàng)建出一個新的FlutterEngine,我們?nèi)绾潍@取到FlutterEngine對象呢晋涣?FlutterFragment中定義了一個getFlutterEngine()
方法仪媒,從方法名來看大概就是獲取FlutterEngine對象。我嘗試過創(chuàng)建MethodChannel時傳入flutterFragment.getFlutterEngine().getDartExecutor()
谢鹊,運行后會直接拋出空指針異常算吩,異常產(chǎn)生的位置在FlutterFragment的getFlutterEngine()
方法中:
@Nullable
public FlutterEngine getFlutterEngine() {
return delegate.getFlutterEngine();
}
錯誤原因是這里的delegate為null,全局搜索一下佃扼,發(fā)現(xiàn)在FlutterFragment的onAttach()
方法中會對delegate賦值偎巢,也就是說明此時沒有執(zhí)行onAttach()
方法。我猜測這就是由于上面提到過的FlutterEngine的warm-up機制兼耀,這是一個耗時過程压昼,因此FlutterFragment并不會立刻執(zhí)行onAttach()
方法,導致我們在Activity的onCreate()
方法中直接使用FlutterFragment的getFlutterEngine()
方法會拋出異常瘤运。目前我也沒想到有什么解決方案窍霞,如果我們要利用FlutterFragment來進行交互,還是只能使用withCachedEngine()
方法來創(chuàng)建FlutterFragment尽超,在構(gòu)造MethodChannel時傳入創(chuàng)建好的FlutterEngine對象官撼。
到這里Flutter 1.12中關(guān)于原生交互的幾個變更基本上就介紹得差不多了,相關(guān)代碼我也已經(jīng)更新似谁。最后還是要感嘆一下Flutter的更新速度傲绣,才幾個月沒看就變化這么大掠哥,之后的版本可能還會修改,如果我了解到有什么變更會及時更新文章秃诵,大家有什么發(fā)現(xiàn)也歡迎提出续搀。
關(guān)于AndroidX
現(xiàn)在越來越多的Android項目都使用了AndroidX庫,之前寫這篇文章時由于還是使用的support庫菠净,因此這里簡單介紹一下AndroidX的遷移吓懈。
關(guān)于Android原生項目的遷移我就不介紹了歼培,相信大家接觸過Android開發(fā)的都有了解過行贪,網(wǎng)上也有很多相關(guān)文章芬为。在Flutter版本1.12.13之后,新建的Module默認就是使用AndroidX庫的攀唯,那么如何把現(xiàn)有的Flutter Module遷移到AndroidX呢洁桌,其實很簡單,在Flutter Module根目錄下的pubspec.yaml文件中添加下面的配置就可以了:
module:
androidX: true // Add this line.
添加之后在命令行執(zhí)行flutter clean
命令即可侯嘀,執(zhí)行完成后我們打開.android(或android)目錄下的gradle.properties文件另凌,會看到添加了如下配置,這就說明Flutter Module已經(jīng)成功遷移到了AndroidX庫戒幔。
當我們把Flutter Module和Android項目都遷移到AndroidX庫之后會發(fā)現(xiàn)代碼中有報錯:
可以看到是在調(diào)用FragmentTransaction的add()
方法時報的錯吠谢,報錯的原因就是因為這里傳入的參數(shù)類型(flutterFragment類型)不正確,我們可以點開FlutterFragment類看一下诗茎,發(fā)現(xiàn)在類文件中定義的FlutterFragment類還是繼承自support包下的Fragment工坊,那么這里當然就會報錯了。先別慌错沃,當你嘗試運行項目時會發(fā)現(xiàn)可以正常運行栅组,這是為什么呢?其實一開始我也有寫疑惑枢析,后來仔細想了想大概是android.enableJetifier=true這個配置的作用,上面也見到過刃麸,是項目遷移到AndroidX庫后自動添加的醒叁,大家可能知道它的作用是將項目中的第三方庫自動遷移到AndroidX庫,因此雖然我們從源碼中看到的FlutterFragment類還是使用的support包泊业,但是gardle在編譯時已經(jīng)自動將FlutterFragment類遷移到了AndroidX庫把沼,所以運行時不會報錯。
關(guān)于AndroidX庫遷移的具體內(nèi)容大家也可以查看官方文檔AndroidX Migration吁伺。
目前最新版本(1.18.0)的Flutter中饮睬,F(xiàn)lutterFragment已經(jīng)改為繼承自androidx包下的Fragment了,編輯器也不會報錯了篮奄,不過具體是哪個版本修改的我就不清楚了捆愁,如果大家有知道的歡迎提出割去。
總結(jié)
本文介紹了Android項目中引入Flutter的方法以及簡單交互場景的實現(xiàn)。
1.Android項目引入Flutter本質(zhì)上是將Flutter編寫的Widget嵌入到Activity中昼丑,類似于WebView呻逆,容器Activity相當于WebView,route相當于url菩帝,有兩種方式FlutterView和FlutterFragment咖城。頁面間的跳轉(zhuǎn)和傳參可以借助MethodChannel來實現(xiàn)。
2.關(guān)于MethodChannel呼奢,它的作用是Flutter和原生方法的互相調(diào)用宜雀,使用時在兩端都要定義MethodChannel,通過相同的name聯(lián)系起來握础,調(diào)用方使用invokeMethod()
州袒,傳入方法名和參數(shù);被調(diào)用方定義MethodCallHandler回調(diào)弓候,根據(jù)方法名和方法參數(shù)執(zhí)行相應的平臺代碼郎哭。
3.本文中所提到的一些方案可能并不是最好的,如果大家有自己的見解歡迎一起交流學習菇存。此外夸研,文中的一些代碼只展示了部分,Demo我已經(jīng)上傳到了github依鸥,大家如果需要的話可以查看亥至。
4.最后提一下flutter_boost,這是閑魚團隊開源的一個Flutter混合開發(fā)插件贱迟,我簡單地嘗試了一下姐扮,還是挺好用的,在頁面跳轉(zhuǎn)和傳參方面都很方便衣吠,大家感興趣的話可以了解一下茶敏。
參考文章
Add Flutter to existing app
Flutter混編:在Android原生中混編Flutter
flutter接入現(xiàn)有的app詳細介紹