Flutter之在Flutter佈局中嵌入原生元件Android篇

NightFarmer發表於2018-12-07

之前介紹過在原生工程內嵌入Flutter,以頁面形式或者View的形式嵌入都是可以的,最近看Flutter原始碼發現Flutter還支援在Flutter佈局中嵌入原生View,這個特性在文件中還沒有介紹,但是確實是一個非常實用的特性,比如困擾已久的地圖實現,有了這個特性我們就可以在Flutter佈局中嵌入雙平臺的原生高德地圖或百度地圖,甚至是相機預覽視訊通話SDK等。
本篇一個簡單的TextView為示例,介紹如何在Flutter工程中嵌入原生元件。

圖片預覽

建立Flutter工程

原生元件擴充套件比較規範的寫法是建立外掛工程,然後讓Flutter工程引入外掛工程使用,本篇為了方便,直接在Flutter工程編寫元件並註冊,外掛工程的開發以後再介紹。
使用AndroidStudio建立一個普通的Flutter工程,修改main.dar檔案,移除不必要的程式碼便於演示,整理後程式碼如下:

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(

      ),
    );
  }

}
複製程式碼

在Android工程中編寫並註冊原生元件

新增原生元件的流程基本是這樣的:
1.實現原生元件PlatformView提供原生view 2.建立PlatformViewFactory用於生成PlatformView 3.建立FlutterPlugin用於註冊原生元件

建立原生元件

在FLutter工程生成了幾個資料夾,lib是放Flutter工程程式碼,android和ios資料夾分別是對應的雙平臺的原生工程,這裡直接開啟Android工程目錄,專案預設生成了GeneratedPluginRegistrant和MainActivity兩個檔案,GeneratedPluginRegistrant不要動,在和MainActivity的包下新建自定義View,Flutter的原生View不能直接繼承自View,需要實現提供的PlatformView介面:

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。

建立PlatformViewFactory

接下來建立PlatformViewFactory,建立一個類繼承自PlatformViewFactory:

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傳過來的自定義引數,這裡暫時用不到。

註冊外掛

建立一個外掛類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中,為了和外掛工程的註冊流程保持一致,還是建議抽出來寫。

在Flutter工程中呼叫原生View

原生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包,並判斷defaultTargetPlatformTargetPlatform.android還是TargetPlatform.iOS來引入不同平臺的實現。

給原生view增加引數

某些情況下,需要給原生元件提供一些初始化引數,比如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來實現,看下面

通過MethodChannel與原生元件通訊

首先讓原始元件實現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/

相關文章