以前介紹過在原生工程內嵌入Flutter,以頁面形式或者View的形式嵌入都是能夠的,最近看Flutter源碼發現Flutter還支持在Flutter佈局中嵌入原生View,這個特性在文檔中尚未介紹,可是確實是一個很是實用的特性,好比困擾已久的地圖實現,有了這個特性咱們就能夠在Flutter佈局中嵌入雙平臺的原生高德地圖或百度地圖,甚至是相機預覽視頻通話SDK等。
本篇一個簡單的TextView爲示例,介紹如何在Flutter工程中嵌入原生組件。
android
原生組件擴展比較規範的寫法是建立插件工程,而後讓Flutter工程引入插件工程使用,本篇爲了方便,直接在Flutter工程編寫組件並註冊,插件工程的開發之後再介紹。
使用AndroidStudio建立一個普通的Flutter工程,修改main.dar文件,移除沒必要要的代碼便於演示,整理後代碼以下:ios
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
),
);
}
}
複製代碼
添加原生組件的流程基本是這樣的:
1.實現原生組件PlatformView提供原生view 2.建立PlatformViewFactory用於生成PlatformView 3.建立FlutterPlugin用於註冊原生組件web
在FLutter工程生成了幾個文件夾,lib是放Flutter工程代碼,android和ios文件夾分別是對應的雙平臺的原生工程,這裏直接打開Android工程目錄,項目默認生成了GeneratedPluginRegistrant和MainActivity兩個文件,GeneratedPluginRegistrant不要動,在和MainActivity的包下新建自定義View,Flutter的原生View不能直接繼承自View,須要實現提供的PlatformView接口:app
public class MyView implements PlatformView {
private final TextView myNativeView;
MyView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
TextView myNativeView = new TextView(context);
myNativeView.setText("我是來自Android的原生TextView");
this.myNativeView = myNativeView;
}
@Override
public View getView() {
return myNativeView;
}
@Override
public void dispose() {
}
}
複製代碼
這是一個包裝類,在實現的getView方法中返回原生的View對象給Flutter,這裏便於演示,返回一個TextView。less
接下來建立PlatformViewFactory,建立一個類繼承自PlatformViewFactory:async
public class MyViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
public MyViewFactory(BinaryMessenger messenger) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
}
@SuppressWarnings("unchecked")
@Override
public PlatformView create(Context context, int id, Object args) {
Map<String, Object> params = (Map<String, Object>) args;
return new MyView(context, messenger, id, params);
}
複製代碼
在create方法中可以獲取到三個參數,args是由Flutter傳過來的自定義參數,這裏暫時用不到。ide
建立一個插件類MyViewFlutterPlugin,並在類的靜態方法中寫上註冊邏輯供調用:佈局
public class MyViewFlutterPlugin {
public static void registerWith(PluginRegistry registry) {
final String key = MyViewFlutterPlugin.class.getCanonicalName();
if (registry.hasPlugin(key)) return;
PluginRegistry.Registrar registrar = registry.registrarFor(key);
registrar.platformViewRegistry().registerViewFactory("plugins.nightfarmer.top/myview", new MyViewFactory(registrar.messenger()));
}
}
複製代碼
上面代碼中使用了plugins.nightfarmer.top/myview
這樣一個字符串,這是組件的註冊名稱,在Flutter調用時須要用到,你可使用任意格式的字符串。 在MainActivity的onCreate方法中增長註冊調用性能
MyViewFlutterPlugin.registerWith(this);
複製代碼
由於這裏是直接在Flutter工程中編寫的,因此也能夠直接把註冊邏輯寫在Activity中,爲了和插件工程的註冊流程保持一致,仍是建議抽出來寫。ui
原生View的調用很是簡單,在使用Android平臺的view只須要建立AndroidView
組件並告訴它組件的註冊註冊名稱便可:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: AndroidView(viewType: 'plugins.nightfarmer.top/myview'),
),
);
}
}
複製代碼
由於只是實現了Android平臺,因此這裏直接調用了AndroidView,若是你是雙平臺的實現,則能夠經過引入package:flutter/foundation.dart
包,並判斷defaultTargetPlatform
是TargetPlatform.android
仍是TargetPlatform.iOS
來引入不一樣平臺的實現。
某些狀況下,須要給原生組件提供一些初始化參數,好比webview的url,好比地圖的中心座標,又好比上面示例的中文本內容,咱們傳入一個map便可實現:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print(defaultTargetPlatform);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: AndroidView(
viewType: 'plugins.nightfarmer.top/myview',
creationParams: {
"myContent": "經過參數傳入的文本內容",
},
creationParamsCodec: const StandardMessageCodec(),
),
),
);
}
}
複製代碼
creationParams
傳入了一個map參數,並由原生組件接收,creationParamsCodec
傳入的是一個編碼對象這是固定寫法。 而後在原生組件中接收參數並初始化TextView的文本:
public class MyView implements PlatformView {
private final TextView myNativeView;
MyView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
TextView myNativeView = new TextView(context);
myNativeView.setText("我是來自Android的原生TextView");
this.myNativeView = myNativeView;
if (params.containsKey("myContent")) {
String myContent = (String) params.get("myContent");
myNativeView.setText(myContent);
}
}
...
}
複製代碼
有一點須要注意的是,原生組件初始化的參數並不會隨着setState重複賦值,也就是說這種是init參數。
關於如何更改已經實例化的原生組件的狀態,能夠經過MethodCall來實現,看下面
首先讓原始組件實現MethodCallHandler
接口:
public class MyView implements PlatformView, MethodChannel.MethodCallHandler {
private final TextView myNativeView;
MyView(Context context, BinaryMessenger messenger, int id, Map<String, Object> params) {
...
MethodChannel methodChannel = new MethodChannel(messenger, "plugins.nightfarmer.top/myview_" + id);
methodChannel.setMethodCallHandler(this);
}
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
// 在接口的回調方法中能夠接收到來自Flutter的調用
}
...
}
複製代碼
而後在dart代碼中作以下處理:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print(defaultTargetPlatform);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: AndroidView(
viewType: 'plugins.nightfarmer.top/myview',
creationParams: {
"myContent": "經過參數傳入的文本內容",
},
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: onMyViewCreated,
),
),
);
}
MethodChannel _channel;
void onMyViewCreated(int id) {
_channel = new MethodChannel('plugins.nightfarmer.top/myview_$id');
setMyViewText();
}
Future<void> setMyViewText(String text) async {
assert(text != null);
return _channel.invokeMethod('setText', text);
}
}
複製代碼
經過onPlatformViewCreated
回調,監聽原始組件成功建立,並可以在回調方法的參數中拿到當前組件的id,這個id是系統隨機分配的,而後經過這個分配的id加上咱們的組件名稱最爲前綴建立一個和組件通信的MethodChannel,拿到channel對象以後就能夠經過invokeMethod方法向原生組件發送消息了,這裏這裏發送的是‘setText’這個消息,並帶上文本內容,下面在原生組件中處理消息的接收邏輯。
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
if ("setText".equals(methodCall.method)) {
String text = (String) methodCall.arguments;
myNativeView.setText(text);
result.success(null);
}
}
複製代碼
onMethodCall的處理方式和正常的插件擴展是一致的,這裏再也不贅述。
經過一個ListView來實例化多個原生組件,看看效果如何:
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
print(defaultTargetPlatform);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: ListView.builder(
itemBuilder: (context, index) {
return Container(
child: AndroidView(
viewType: 'plugins.nightfarmer.top/myview',
creationParams: {
"myContent": "經過參數傳入的文本內容$index",
},
creationParamsCodec: const StandardMessageCodec(),
),
height: 100,
);
},
itemCount: 100,
),
);
}
}
複製代碼
這樣寫雖然跑起來了,ListView也確實可以正常滑動,可是可以感覺到明顯的掉幀,可見在一個界面中實例化多個原生組件的狀況對性能的影響很是的大,也不建議在實際開發中大量引入原生組件,由於除去地圖/WebView等特殊狀況,基本上原生能實現的UI效果Flutter的UI引擎都能實現。
在開發原生組件時,Flutter的熱加載是無效的,由於每次都須要編譯原生工程才能使之生效。另外我這裏的Mac環境用Genymotion是沒法正常運行的,須要使用真機並不使用
--enable-software-rendering
參數才能夠。
本篇完。
更多幹貨移步個人我的博客 www.nightfarmer.top/