作者:余天然
本文主要解決3個問題:
- 集成Flutter到Android項目,可以打開Flutter的默認頁面
- 可以跳轉到Flutter的指定頁面
- 可以將Flutter的指定組件嵌入到原生頁面,并傳遞參數(shù)
1.集成Flutter到Android
這里,我們以Flutter Module創(chuàng)建一個Flutter工程(flutter),然后run起來,就可以在.android/Flutter/build/outouts/aar文件夾下面得到這個aar

這里之所以以Flutter Module模式開發(fā),而不是Flutter Application,就是為了得到這個aar。
Flutter Module模式下自動生成的.android文件夾下,才會有這個Flutter文件夾,F(xiàn)lutter Application則沒有。
這樣的話,我們才可以借用Flutter已經有的生成aar的gradle腳本,不然還得自己去寫gradle打包腳本,很容易踩到坑里就爬不起來了。
然后我們再另開一個窗口,新建一個Android工程(flutter_container),將這個aar復制過去

這里需要注意的一個問題,因為Flutter本身原因,導致復制出來的aar里面缺少icudtl.dat文件,需要我們自己手動復制這個icudtl.dat文件到assets/flutter_shared目錄下。
怎么得到這個icudtl.dat文件呢,很簡單,解壓Flutter工程生成的默認apk即可得到

然后,我們就需要在宿主Android工程里面,建立接收Flutter的Activity了。這里可以借鑒Flutter工程的.android/app目錄,核心就是兩個:
- Application:初始化Flutter
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
FlutterMain.startInitialization(this);
}
}
- Activity:繼承FlutterActivity
/**
* debug模式原生跳轉到flutter界面會出現(xiàn)白屏,release包就不會出現(xiàn)白屏了
*/
public class MainFlutterActivity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
}
}
這樣以后,我們就可以跳轉這個MainFlutterActivity,實現(xiàn)在Android工程里面進入Flutter工程的默認頁面了。
2. 跳轉指定頁面
上面只是簡單集成了Flutter,但是我們知道,我們從Android工程里面跳轉Flutter,肯定是需要選擇性的跳轉指定頁面的,不可能只是簡單的跳轉默認頁面就完了,所以,這里需要用到Flutter的靜態(tài)路由了。
修改Flutter工程的main.dart,定義了兩個指定頁面的路由: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ù),主要是方便原生調用
routes: <String, WidgetBuilder> {
'homePage': (BuildContext context) => new HomePage(),
'channelPage': (BuildContext context) => new ChannelPage(),
},
);
}
}
然后在宿主Android工程下,添加指定頁面的容器Activity,通過Flutter.createView來獲取指定頁面的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);
}
}
這樣以后,我們就可以跳轉這個HomeFlutterActivity,實現(xiàn)在Android工程里面進入Flutter工程的指定頁面了。
3. 嵌入View并傳遞參數(shù)
上面雖然能夠跳轉指定頁面了,但是很顯然,有一個很大的問題:不能傳遞參數(shù)。
這是Flutter的靜態(tài)路由的一個很大的弊端,雖然通過動態(tài)路由可以傳遞參數(shù)和接收返回值,但是動態(tài)路由沒法給原生調用。
Navigator.of(context)
.push<String>(new MaterialPageRoute(builder: (context) {
return new NextPage(params);
})).then((String value) {
setState(() {
params = value;
});
});
有一個Flutter的路由庫:Fluro,可以實現(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內部還行,卻無法給原生調用,在原生里面通過Flutter.createView的時候,是沒法使用Fluro的,只能是默認的路由。
調研了很多方案,最后,沒有辦法了,只好采用最笨的方法:通過MethodChannel來傳遞參數(shù)。
這里需要注意的是MethodChannel的調用,應該FlutterView已經創(chuàng)建完成,所以需要通過flutterView.post(new Runnable())來執(zhí)行了,直接執(zhí)行是不會傳參給Flutter的。
原生傳參給Flutter
原生調用
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.原生調用invokeFlutterMethod-success:"+o.toString());
}
@Override
public void error(String s, @Nullable String s1, @Nullable Object o) {
Log.i("flutter","1.原生調用invokeFlutterMethod-error");
}
@Override
public void notImplemented() {
Log.i("flutter","1.原生調用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調用
print("3.Flutter調用invokeNativeMethod");
int result =
await platform.invokeMethod("invokeNativeMethod", "hello,native");
print("5.收到原生執(zhí)行結果:${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:
}
}
});
最后貼一下這個傳參頁面的完整代碼吧,主要就是跑了一下:
- 原生調用invokeFlutterMethod
- Flutter執(zhí)行invokeFlutterMethod
- Flutter調用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.原生調用Flutter方法
channel.invokeMethod("invokeFlutterMethod", "hello,flutter", new MethodChannel.Result() {
@Override
public void success(@Nullable Object o) {
Log.i("flutter","1.原生調用invokeFlutterMethod-success:"+o.toString());
}
@Override
public void error(String s, @Nullable String s1, @Nullable Object o) {
Log.i("flutter","1.原生調用invokeFlutterMethod-error");
}
@Override
public void notImplemented() {
Log.i("flutter","1.原生調用invokeFlutterMethod-notImplemented");
}
});
Log.i("flutter","1.原生調用invokeFlutterMethod");
//4.Flutter調用原生方法的監(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 ="默認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調用invokeNativeMethod");
int result =
await platform.invokeMethod("invokeNativeMethod", "hello,native");
print("5.收到原生執(zhí)行結果:${result}");
}
}
對啦,我們這節(jié)說的是將Flutter以View級別嵌套在一個Android的Activity里面,其實很簡單了啊,因為我們通過Flutter.createView創(chuàng)建出來的View和普通的View并沒有什么太大的區(qū)別,直接addView就可以了,沒啥特別操作,比如這個ChannelFlutterActivity,我用的布局文件就是如下所示:

最后的執(zhí)行效果就是:

其它坑
1. Flutter工程依賴了插件時,宿主Android工程會報找不到插件的原生代碼的錯誤
我的Flutter工程依賴了shared_preferences插件,導致報錯:

原因是:Flutter工程導出成aar的時候,沒有包含插件里面的原生代碼。
解決方案有2種,網上說是不用默認的生成aar的方式,用fataar-gradle-plugin來讓生成的flutter.aar直接包含嵌套的插件工程的aar,這就需要修改Flutter工程的.android/Flutter/build.gradle文件了。我試過,結果報了循環(huán)依賴的錯誤,就放棄了,大家如果這個方案走通了,歡迎告知我具體步驟。
我的解決方案:這里我采取了一個簡單粗暴直接的方案,直接找到插件的aar,將它也復制到宿主Android工程了。這個插件的aar在這里:

復制到這里:

但是這個方案的弊端就是,以后每一個插件,你都需要復制一下,后期的維護成本是有點高的。不像fataar是一勞永逸,只有flutter.aar這一份aar的。尤其是后期肯定會將aar做成遠程依賴,而不再是直接發(fā)復制過去,那維護成本就更高了些。
結語
通過上文可以看到,其實Flutter集成到Android項目還是挺方便的(除了FlutterView傳參有點麻煩)。至于Flutter如何集成到ios項目,我還沒有實踐過,還需要和ios的同事探索,如果你在集成到ios項目的過程中,填了哪些坑,有哪些經驗總結,歡迎和我們交流。