Flutter之在Flutter布局中嵌入原生组件Android篇

6,488 阅读5分钟

之前介绍过在原生工程内嵌入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/