Flutter 与 Android 的交互

该文已授权公众号 「码个蛋」,转载请指明出处

Flutter 说到底只是一个 UI 框架,很多功能都需要通过原生的 Api 来实现,那么就会涉及到 Flutter 和 Native 的交互,因为本人不懂 iOS 开发,所以只能讲下 Flutter 同 Android 的交互。

Android 项目配置 Flutter 依赖

既然是互相交互,那么需要准备一个 Android 项目。接着就需要创建 flutter module,让 Android 项目依赖,创建的方法可以参考官网 Flutter Wiki,虽然是官网提供的方法,但是完全按照这个步骤来,还是会有坑的,这边就慢慢一步步解决坑。

如果你用的是 Android Studio 进行开发的话,直接打开底部的 Terminal,直接创建 flutter module 依赖

flutter create -t module flutter_native_contact 至于 module 名可以随意填写,module 创建完后结构大概是这样的

flutter module.png

接着切换到 module 下的 .android 文件夹,接着有坑来了,官网提供的方法是 ./gradlew flutter:assembleDebug 可能会提示命令不存在,那么直接通过 gradlew flutter:assembleDebug 来运行,等它自动跑完后,打开根目录下的 settings.gradle 文件,加入官网提供的 gradle 代码

setBinding(new Binding([gradle: this]))                                 // new
evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'flutter_native_contact/.android/include_flutter.groovy'              // new
))                                                                      // new

你以为这里没坑,真是图样图森破,没坑是不可能的,编译器大爷可能会给你甩这么个错误

error.png

很明显可以看出是找不到我们的文件,所以把文件名路径给补全

evaluate(new File(                                                      // new
  settingsDir.parentFile,                                               // new
  'FlutterNativeContactDemo/flutter_native_contact/.android/include_flutter.groovy' // 这里补全路径
))

接着打开原有项目下,原有项目下,原有项目下的 app 中的 build.gradle 文件,在 android 下加上如下代码

compileOptions {
  sourceCompatibility 1.8
  targetCompatibility 1.8
}

这个必须要加,不要问为什么,我也不知道为什么,最后在项目下添加 flutter module 的依赖就完成了。这个过程告诉我们一个什么道理呢?*不要以为官网的都对,官网讲的也不是完全可信的,时不时给你来个坑就能卡你老半天。

原生界面加载 Flutter 页面

那么如何在原生界面显示 Flutter 界面呢,这个就需要通过 FlutterView 来实现了,Flutter 这个类提供了 createViewcreateFragment 两个方法,分别用于返回 FlutterView 和 FlutterFragment 实例,FlutterFragment 的实现原理也是通过 FlutterView 来实现的,可以简单看下 FlutterFragment 的源码

/**
 * A {@link Fragment} managing a {@link FlutterView}.
 *
 * <p><strong>Warning:</strong> This file is auto-generated by Flutter tooling.
 * DO NOT EDIT.</p>
 */
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 FlutterView onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    // 最后还是挺过 createView 方法来生成页面,只不过直接放在 fragment,
    // 放在 fragment 会比直接 使用 FlutterView 更方便管理,例如实现 ViewPager 等
    return Flutter.createView(getActivity(), getLifecycle(), mRoute);
  }
}
createFragment 方式加载

在原生页面显示 Flutter 界面的第一种方式就是加载 FlutterFragment,看个比较简单的例子吧

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 这个布局用于加载 fragment -->
    <FrameLayout
        android:id="@+id/fragment_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <android.support.design.widget.FloatingActionButton
        android:id="@+id/flutter_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="20dp"
        android:layout_marginBottom="50dp"
        android:src="@drawable/ic_add_white_36dp"
        app:fabSize="auto"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent" />

</android.support.constraint.ConstraintLayout>

在 Activity 可以直接通过返回 FlutterFragment 加载到 FrameLayout 即可

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        supportFragmentManager.beginTransaction()
            .add(R.id.fragment_container, Flutter.createFragment("route_flutter"))
            .commit()
    }
}

这样就把 Flutter 页面加载到原生界面了,会通过传递的路由值在 dart 层进行查找,所以接着就需要编写 Flutter 界面

/// runApp 内部值也可以直接传入 _buildWidgetForNativeRoute 方法
/// 这边在外层嵌套一层 MaterialApp 主要是防止一些不必要的麻烦,
/// 例如 MediaQuery 这方面的使用等
void main() => runApp(FlutterApp());

class FlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: _buildWidgetForNativeRoute(window.defaultRouteName),
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primaryColor: Color(0XFF008577),
        accentColor: Color(0xFFD81B60),
        primaryColorDark: Color(0xFF00574B),
        iconTheme: IconThemeData(color: Color(0xFFD81B60)),
      ),
    );
  }
}

/// 该方法用于判断原生界面传递过来的路由值,加载不同的页面
Widget _buildWidgetForNativeRoute(String route) {
  switch (route) {
    case 'route_flutter':
      return GreetFlutterPage();
    // 默认的路由值为 '/',所以在 default 情况也需要返回页面,否则 dart 会报错,这里默认返回空页面
    default: 
      return Scaffold();
  }
}

class GreetFlutterPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('NativeMessageContactPage'),
      ),
      body: Center(
        child: Text(
          'This is a flutter fragment page',
          style: TextStyle(fontSize: 20.0, color: Colors.black),
        ),
      ),
    );
  }
}

运行后可以看到页面加载出来了,不过会有一段时间的空白,这个在正式打包后就不会出现,所以不必担心。最后的页面应该是这样的

flutter fragment.png
createView 方式加载

接着看下 createView 方法,说白了,第一种方法最后还是会通过该方式实现

  @NonNull
  public static FlutterView createView(@NonNull final Activity activity, @NonNull final Lifecycle lifecycle, final String initialRoute) {
    // 交互前的一些初始化工作,需要完成才可以继续下一步,同时需要保证当前线程为主线程
    // Looper.myLooper() == Looper.getMainLooper(),否则会甩你一脸的 IllegalStateException 
    FlutterMain.startInitialization(activity.getApplicationContext());
    FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), null);
    final FlutterNativeView nativeView = new FlutterNativeView(activity);
    // 将 flutter 页面绑定到相应的 activity
    final FlutterView flutterView = new FlutterView(activity, null, nativeView) {
        // ......
    };
    // 将路由值传到 flutter 层,并加载相应的页面,
    if (initialRoute != null) {
      flutterView.setInitialRoute(initialRoute);
    }
    
    // 绑定 lifecycle,方便生命周期管理,同 activity 绑定
    // 不熟悉 LifeCycle 的同学可以自行网上查找资料
    lifecycle.addObserver(new LifecycleObserver() {
      @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
      public void onCreate() {
        // 配置一些参数,传递到 flutter 层
        final FlutterRunArguments arguments = new FlutterRunArguments();
        arguments.bundlePath = FlutterMain.findAppBundlePath(activity.getApplicationContext());
        arguments.entrypoint = "main";
        // 最终会调用方法 nativeRunBundleAndSnapshotFromLibrary,这是一个 native 方法,进行交互
        flutterView.runFromBundle(arguments);
        // 进行注册
        GeneratedPluginRegistrant.registerWith(flutterView.getPluginRegistry());
      }
    // ......
    });

    return flutterView;
  }

通过 createView 方法返回的 FlutterView,通过设置 Layoutparams 参数就可以添加到相应的布局上,还有一种直接通过 addContentView 方式进行加载,这里直接修改原有代码,

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // setContentView(R.layout.activity_main) 不需要这一步了
        val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_flutter")
        val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
        addContentView(flutterView, lp) // 直接加载到 activity 页面
    }

但是通过这样加载的话,那么整个页面都是 flutter 的页面。那么之前的效果的 FAB 则不会被加载出来了,即使没有省略 setContentView(R.layout.activity_main) 方法,这个页面的 xml 布局也会被覆盖。

PlantformChannel

那么能够在原生界面显示 flutter 页面了,如何互相交互呢,这就需要通过 PlantformChannel 来执行了,PlantformChannel 主要有三种类型,BasicMessageChannel,MethodChannel,EventChannel。通过查看源码可以发现,三个 Channel 的实现机制类似,都是通过 BinaryMessenger 进行信息交流,每个 Channel 通过传入的 channel name 进行区分,所以在注册 Channel 的时候必须要保证 channel name 是唯一的,同时需要传入一个 BinaryMessageHandler 实例,用于传递信息的处理,当 Handler 处理完信息后,会返回一个 result,然后通过 BinaryMessenger 将 result 返回到 Flutter 层。如果需要深入理解这边推荐一篇文章深入理解Flutter PlatformChannel

接下来直接看例子吧,在创建 PlatformChannel 的时候需要传入一个 BinaryMessenger 实例,通过查看 FlutterView 的源码可以发现,FlutterView 就是一个 BinaryMessenger 在 Android 端的实现,所以呢,可以直接通过前面介绍的 Flutter.createView 方法获取注册 Channel 时的 BinaryMessenger 实例了,真是得来全部费工夫~因为通信的方法可能在多个界面会使用,所以还是封装一个通用类来处理会比较合理

BasicMessageChannel

BasicMessageChannel 用于传递字符串和半结构化的信息。

class FlutterPlugin(private val flutterView: FlutterView) :BasicMessageChannel.MessageHandler<Any>{
    companion object {
        private const val TAG = "FlutterPlugin"

        @JvmStatic
        fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
            // channel name 需要保持两侧一致
            val messageChannel =
               BasicMessageChannel(flutterView, Constant.MESSAGE_CHANNEL_NAME, StandardMessageCodec.INSTANCE) // MessageCodec 有多种实现方式,可以参考推荐的文章

            val instance = FlutterPlugin(flutterView)
            messageChannel.setMessageHandler(instance) // 注册处理的 Hnadler

            return instance
        }
    }

    override fun onMessage(`object`: Any?, reply: BasicMessageChannel.Reply<Any>?) {
        // 简单的将从 Flutter 传过来的消息进行吐司,同时返回自己的交互信息
        // `object` 中包含的就是 Flutter 层传递过来的信息,reply 实例用于传递信息到 Flutter 层
        Toast.makeText(flutterView.context, `object`.toString(), Toast.LENGTH_LONG).show()
        reply?.reply("\"Hello Flutter\"--- an message from Android")
    }
}

接着就需要有个 FlutterView 用来注册,新建一个 Activity,用于加载 Flutter 页面

class ContactActivity : AppCompatActivity() {
    private lateinit var plugin: FlutterPlugin

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // 传入路由值,需要在 flutter 层生成相应的界面
        val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_contact")
        val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
        addContentView(flutterView, lp)

        plugin = FlutterPlugin.registerPlugin(flutterView)
    }

    override fun onDestroy() {
        super.onDestroy()
    }
}

那么我们就要在 Flutter 界面的 _buildWidgetForNativeRoute 方法加入新路由值对应的界面

Widget _buildWidgetForNativeRoute(String route) {
  switch (route) {
    // ...
          
    case 'route_contact':
      return FlutterContactPage();

    default:
      return Scaffold();
  }
}

class FlutterContactPage extends StatelessWidget {
  // 注册对应的 channel,要保证 channel name 和原生层是一致的
  final BasicMessageChannel _messageChannel =
      BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter Page'),
      ),
      // 简单放一个按钮,通过 channel 传输消息过去,同时将原生层返回的消息打印出来
      body: RaisedButton(
        onPressed: () {
          _messageChannel
              .send('"Hello Native" --- an message from flutter')
              .then((str) {
            print('Receive message: $str');
          });
        },
        child: Text('Send Message to Native'),
      ),
    );
  }
}

最后的效果小伙伴可以自行执行,点击按钮后会弹出吐司,吐司内容就是 Flutter 传递的信息,同时在控制台可以看到从原生层返回的信息。

MethodChannel

MethodChannel 用于传递方法调用(method invocation)

直接在上述例子中进行修改,例如在 Flutter 页面中实现 Activity 的 finish 方法,并传递参数到前一个界面,先做 Flutter 页面的修改,在 AppBar 上增加一个返回按钮,用于返回上层页面

class FlutterContactPage extends StatelessWidget {
  // 注册对应的 channel,要保证 channel name 和原生层是一致的
  final BasicMessageChannel _messageChannel =
      BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());
  final MethodChannel _methodChannel = MethodChannel(METHOD_CHANNEL_NAME);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: InkWell(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 20.0),
            child: Icon(Icons.arrow_back),
          ),
          onTap: () {
            _methodChannel
                // invokeMethod 第一个值用于传递方法名,第二个值用于传递参数,
                // 这边简单的传递一个字符串,当然也可以传递别的类型,map,list 等等
                .invokeMethod<bool>('finishActivity', 'Finish Activity')
                .then((result) { // 这边会返回一个结果值,通过判断是否成功来打印不同的信息
              print('${result ? 'has finish' : 'not finish'}');
            });
          },
        ),
        title: Text('Flutter Page'),
      ),
        
      body: // ...
    );
  }
}

同时,我们需要在 FlutterPlugin 这个类中,做些必要的修改,首先需要实现 MethodCallHandler 接口,该接口中需要实现 onMethodCall 方法,通过获取调用的方法名和参数值,进行相应的处理

class FlutterPlugin(private val flutterView: FlutterView) :
    MethodChannel.MethodCallHandler, BasicMessageChannel.MessageHandler<Any> {

    companion object {
        private const val TAG = "FlutterPlugin"

        @JvmStatic
        fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
            val instance = FlutterPlugin(flutterView)
            val methodChannel = MethodChannel(flutterView, Constant.METHOD_CHANNEL_NAME)
            // ...
            messageChannel.setMessageHandler(instance)
            return instance
        }
    }
        
    // ....

    // call 中携带了 Flutter 层传递过来的方法名和参数信息
    // 可以分别通过 call.method 和 call.arguments 来获取
    override fun onMethodCall(call: MethodCall?, result: MethodChannel.Result?) {
        when (call?.method) {
            "finishActivity" -> {
                val activity = flutterView.context as Activity
                val info = call.arguments.toString()
                
                val intent = Intent().apply {
                    putExtra("info", info)
                }

                activity.setResult(Activity.RESULT_OK, intent)
                activity.finish()
                
                // 成功时候通过 result.success 返回值,
                // 如果发生异常,通过 result.error 返回异常信息
                // Flutter 通过 invokeMethod().then() 来处理正常结束的逻辑
                // 通过 catchError 来处理发生异常的逻辑
                result?.success(true)
            }

            // 如果未找到对应的方法名,则通过 result.notImplemented 来返回异常
            else -> result?.notImplemented()
        }
    }

最终的效果,当点击返回按钮的时候,会将 Flutter 层通过 invokeMethod 传递的 arguments 属性吐司出来,同时,控制台会打印出 "has finish" 的信息

EventChannel

EventChannel 用于数据流(event streams)的通信

EventChannel 的实现方式也类似,EventChannel 可以持续返回多个信息到 Flutter 层,在 Flutter 层的表现就是一个 stream,原生层通过 sink 不断的添加数据,Flutter 层接收到数据的变化就会作出新相应的处理。在 Android 端实现状态的监听可以通过广播来实现。直接看例子,还是修改上述代码

class FlutterPlugin(private val flutterView: FlutterView) :
    MethodChannel.MethodCallHandler, EventChannel.StreamHandler, BasicMessageChannel.MessageHandler<Any> {

    private var mStateChangeReceiver: BroadcastReceiver? = null

    companion object {
        private const val TAG = "FlutterPlugin"
        const val STATE_CHANGE_ACTION = "com.demo.plugins.action.StateChangeAction"
        const val STATE_VALUE = "com.demo.plugins.value.StateValue"

        @JvmStatic
        fun registerPlugin(flutterView: FlutterView): FlutterPlugin {
            // ... 
            val streamChannel = EventChannel(flutterView, Constant.STREAM_CHANNEL_NAME)

            val instance = FlutterPlugin(flutterView)
            methodChannel.setMethodCallHandler(instance)
            streamChannel.setStreamHandler(instance)
            messageChannel.setMessageHandler(instance)

            return instance
        }
    }

    // 实现 StreamHandler 需要重写 onListen 和 onCancel 方法
    // onListen 不会每次数据改变就会调用,只在 Flutter 层,eventChannel 订阅广播
    // 的时候调用,当取消订阅的时候则会调用 onCancel,
    // 所以当开始订阅数据的时候,注册接收数据变化的关闭,
    // 在取消订阅的时候,将注册的广播注销,防止内存泄漏
    override fun onListen(argument: Any?, sink: EventChannel.EventSink?) {
        mStateChangeReceiver = createEventListener(sink)
        flutterView.context.registerReceiver(mStateChangeReceiver, IntentFilter(STATE_CHANGE_ACTION))
    }

    override fun onCancel(argument: Any?) {
        unregisterListener()
    }

    // 在 activity 被销毁的时候,FlutterView 不一定会调用销毁生命周期,或者会延时调用
    // 这就需要手动去注销一开始注册的广播了
    fun unregisterListener() {
        if (mStateChangeReceiver != null) {
            flutterView.context.unregisterReceiver(mStateChangeReceiver)
            mStateChangeReceiver = null
        }
    }

    private fun createEventListener(sink: EventChannel.EventSink?):
            BroadcastReceiver = object : BroadcastReceiver() {

        override fun onReceive(context: Context?, intent: Intent?) {
            if (TextUtils.equals(intent?.action, STATE_CHANGE_ACTION)) {
                // 这边广播只做简单的接收一个整数,然后通过 sink 传递到 Flutter 层
                // 当然,sink 还有 error 方法,用于传递发生的错误信息,
                // 以及 endOfStream 方法,用于结束接收
                // 在 Flutter 层分别有 onData 对应 success 方法,onError 对应 error 方法
                // onDone 对应 endOfStream 方法,根据不同的回调处理不同的逻辑
                sink?.success(intent?.getIntExtra(STATE_VALUE, -1))
            }
        }
    }
}

在 Flutter 层,通过对 stream 的监听,对返回的数据进行处理,为了体现出变化,这边修改成 SatefulWidget 来存储状态

class FlutterContactPage extends StatefulWidget {
  @override
  _FlutterContactPageState createState() => _FlutterContactPageState();
}

class _FlutterContactPageState extends State<FlutterContactPage> {
  final MethodChannel _methodChannel = MethodChannel(METHOD_CHANNEL_NAME);
  final EventChannel _eventChannel = EventChannel(STREAM_CHANNEL_NAME);
  final BasicMessageChannel _messageChannel =
      BasicMessageChannel(MESSAGE_CHANNEL_NAME, StandardMessageCodec());
  StreamSubscription _subscription;
  var _receiverMessage = 'Start receive state'; // 初始的状态值

  @override
  void initState() {
    super.initState();
    // 当页面生成的时候就开始监听数据的变化
    _subscription = _eventChannel.receiveBroadcastStream().listen((data) {
      setState(() {
        _receiverMessage = 'receive state value: $data'; // 数据变化了,则修改数据
      });
    }, onError: (e) {
      _receiverMessage = 'process error: $e'; // 发生错误则显示错误信息
    }, onDone: () {
      _receiverMessage = 'receive data done'; // 发送完毕则直接显示完毕
    }, cancelOnError: true);
  }

  @override
  void dispose() {
    super.dispose();
    _subscription.cancel(); // 当页面销毁的时候需要将订阅取消,防止内存泄漏
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        leading: InkWell(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 20.0),
            child: Icon(Icons.arrow_back),
          ),
          onTap: () {
            // MethodChannel demo
            _methodChannel
                .invokeMethod<bool>('finishActivity', _receiverMessage)
                .then((result) {
              print('${result ? 'has finish' : 'not finish'}');
            }).catchError((e) {
              print('error happend: $e');
            });
          },
        ),
        title: Text('Flutter Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              // EventChannel demo,页面直接显示信息的变化
              child: Text(
                _receiverMessage,
                style: TextStyle(fontSize: 20.0, color: Colors.black),
              ),
            ),
            // BasicMessageChannel demo
            RaisedButton(
              onPressed: () {
                _messageChannel
                    .send('"Hello Native" --- an message from flutter')
                    .then((str) {
                  print('Receive message: $str');
                });
              },
              child: Text('Send Message to Native'),
            ),
          ],
        ),
      ),
    );
  }
}

同时,需要在 Activity 层调用一个定时任务不断的发送广播

class ContactActivity : AppCompatActivity() {

    private var timer: Timer? = null
    private var task: TimerTask? = null
    private lateinit var random: Random
    private lateinit var plugin: FlutterPlugin

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        random = Random() // 生成随机整数
        val flutterView = Flutter.createView(this@ContactActivity, lifecycle, "route_contact")
        val lp = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
        addContentView(flutterView, lp)

        plugin = FlutterPlugin.registerPlugin(flutterView)

        timer = Timer() // 定时器
        task = timerTask { // 定时任务
            sendBroadcast(Intent(FlutterPlugin.STATE_CHANGE_ACTION).apply {
                putExtra(FlutterPlugin.STATE_VALUE, random.nextInt(1000))
            })
        }
        timer?.schedule(task, 3000, 2000) // 延时 3s 开启定时器,并 2s 发送一次广播
    }

    override fun onDestroy() {
        super.onDestroy()

        // 页面销毁的时候需要将定时器,定时任务销毁
        // 同时注销 Plugin 中注册的广播,防止内存泄漏
        timer?.cancel()
        timer = null

        task?.cancel()
        task = null

        plugin.unregisterListener()
    }
}

最后的实现效果大概是这样的

event channel.gif

Flutter 同 Android 端的交互到这讲的差不多了,和 iOS 的交互其实也类似,只不过在 Android 端通过 FlutterNativeView 来作为 Binarymessenger 的实现,在 iOS 端通过 FlutterBinaryMessenger 协议实现,原理是一致的。至于 Flutter 插件,其实现也是通过以上三种交互方式来实现的,可能我们目前通过 FlutterView 来作为 BinaryMessenger 实例,插件会通过 PluginRegistry.Registrar 实例的 messenger() 方法来获取 BinaryMessenger 实例。

需要了解插件的写法也可以直接查看官方提供的检测电量插件:Flutter Battery Plugin

在 Flutter 上显示原生的控件

在日常开发过程中,可能会遇到这么一种情况,Flutter 中没有控件,但是在原生有,比如地图控件,那么就需要在 Flutter 显示原生的控件了,那么就需要用到 AndroidViewUiKitView 来加载原生的控件,这边以 GoogleMapPlugin 为例

class _GoogleMapState extends State<GoogleMap> {
  // 省略部分代码
    
  @override
  Widget build(BuildContext context) {
    // 省略部分代码
    // 判断当前设备是否 android 设备,或者 iOS 设备
    if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(
        viewType: 'plugins.flutter.io/google_maps', // viewType 需要同原生端对应,来加载对应的 view
        onPlatformViewCreated: onPlatformViewCreated,
        // ....
      );
    } else if (defaultTargetPlatform == TargetPlatform.iOS) {
      return UiKitView(
        viewType: 'plugins.flutter.io/google_maps',
        onPlatformViewCreated: onPlatformViewCreated,
        // .... 
      );
    }

    return Text(
        '$defaultTargetPlatform is not yet supported by the maps plugin');
  }

这边只贴出关键部分的代码,多余的代码省略,完整代码可以通过上述链接查看

接着看下 Android 端的代码

public class GoogleMapsPlugin implements Application.ActivityLifecycleCallbacks {
  // 省略部分代码

  public static void registerWith(Registrar registrar) {
    if (registrar.activity() == null) {
      // When a background flutter view tries to register the plugin, the registrar has no activity.
      // We stop the registration process as this plugin is foreground only.
      return;
    }
    final GoogleMapsPlugin plugin = new GoogleMapsPlugin(registrar);
    registrar.activity().getApplication().registerActivityLifecycleCallbacks(plugin);
    // 通过 registerViewFactory 方法注册相应的 PlatformViewFactory,
    // 其中第一个参数就是 Flutter 端对应的 viewType 参数值
    registrar.platformViewRegistry()
        .registerViewFactory(
            "plugins.flutter.io/google_maps", new GoogleMapFactory(plugin.state, registrar));
  }
    
  // 省略部分代码
}

那么所有的显示工作都放到 GoogleMapFactory 这个类中了

public class GoogleMapFactory extends PlatformViewFactory {
  // 省略部分代码
    
  @SuppressWarnings("unchecked")
  @Override
  public PlatformView create(Context context, int id, Object args) {
    Map<String, Object> params = (Map<String, Object>) args;
    final GoogleMapBuilder builder = new GoogleMapBuilder();

    // 省略属性设置代码
    // 通过  `GoogleMapBuilder` 设置一些初始属性   
    return builder.build(id, context, mActivityState, mPluginRegistrar);
  }
}

GoogleMapFactory 继承 PlatformViewFactory 并重写 create 方法,返回一个 PlatformView 实例,这个实例通过 GoogleMapBuilder 进行初始化

// GoogleMapOptionsSink -> Receiver of GoogleMap configuration options.
class GoogleMapBuilder implements GoogleMapOptionsSink {
  // 省略部分代码 
   
  GoogleMapController build(
      int id, Context context, AtomicInteger state, PluginRegistry.Registrar registrar) {
    final GoogleMapController controller =
        new GoogleMapController(id, context, state, registrar, options);
    controller.init();
    controller.setMyLocationEnabled(myLocationEnabled);
    controller.setMyLocationButtonEnabled(myLocationButtonEnabled);
    controller.setIndoorEnabled(indoorEnabled);
    controller.setTrafficEnabled(trafficEnabled);
    controller.setTrackCameraPosition(trackCameraPosition);
    controller.setInitialMarkers(initialMarkers);
    controller.setInitialPolygons(initialPolygons);
    controller.setInitialPolylines(initialPolylines);
    controller.setInitialCircles(initialCircles);
    controller.setPadding(padding.top, padding.left, padding.bottom, padding.right);
    return controller;
  }
    
  // 省略部分 set 方法代码
}

GoogleMapBuilder 实现了GoogleMapOptionsSink 这个接口,主要用于接收一些地图属性参数,通过 build 方法最终返回的是一个 GoogleMapController 实例

final class GoogleMapController
    implements Application.ActivityLifecycleCallbacks,
        // 这里省略了一些地图处理的相关接口
        MethodChannel.MethodCallHandler,
        PlatformView {

  GoogleMapController(
      int id,
      Context context,
      AtomicInteger activityState,
      PluginRegistry.Registrar registrar,
      GoogleMapOptions options) {
    
    // 省略参数 set 代码
    methodChannel =
        new MethodChannel(registrar.messenger(), "plugins.flutter.io/google_maps_" + id);
    methodChannel.setMethodCallHandler(this);
  }

  @Override
  public View getView() {
    return mapView;
  }

  @Override
  public void onMethodCall(MethodCall call, MethodChannel.Result result) {
      // 省略实现代码,switch .. case
  }

  @Override
  public void dispose() {
    if (disposed) {
      return;
    }
    disposed = true;
    methodChannel.setMethodCallHandler(null);
    mapView.onDestroy();
    registrar.activity().getApplication().unregisterActivityLifecycleCallbacks(this);
  }
}

GoogleMapController 这个类实现的接口比较多,这里主要看两个接口

  • MethodChannel.MethodCallHandler 对应实现的方法为 onMethodCall 方法,这里就是用于处理 Flutter 层调用原生的方法了,和前面介绍交互的一致
  • PlatformView 对应实现的方法为 getViewdispose 方法,getView 返回一个 View 即为需要在 Flutter 层显示的控件了,dispose 方法用于处理一些生命周期相关的逻辑,销毁会造成内存泄漏的实例

同时在初始化该类的时候,注册了相应的 MethodChannel,用于两端的交互,那么在 Flutter 端是哪里注册的 channel 呢,答案是 controller 文件下的 GoogleMapController

class GoogleMapController {
  GoogleMapController._(
    this.channel,
    CameraPosition initialCameraPosition,
    this._googleMapState,
  ) : assert(channel != null) {
    channel.setMethodCallHandler(_handleMethodCall);
  }

  static Future<GoogleMapController> init(
    int id,
    CameraPosition initialCameraPosition,
    _GoogleMapState googleMapState,
  ) async {
    assert(id != null);
    final MethodChannel channel =
        MethodChannel('plugins.flutter.io/google_maps_$id');
    await channel.invokeMethod<void>('map#waitForMap');
    return GoogleMapController._(
      channel,
      initialCameraPosition,
      googleMapState,
    );
  }

  @visibleForTesting
  final MethodChannel channel;
  
  // 省略无关代码
}

当使用的时候,GoogleMap 只负责显示视图,属性操作通过 GoogleMapController 来进行设置,完美的分担相应的职责

iOS 端的 UiKitView 处理过程也类似,在使用过程中,需要注意

  • 嵌入原生 view 是一个昂贵的操作,所以应当避免在 flutter 能够实现的情况下去使用它
  • 嵌入原生 view 的绘制和其他任何 flutter widge t一样,view 的转换也同样使用
  • 组件会撑满所有可获得控件,因此它的父组件需要提供一个布局边界
  • AndroidView 需要 api 版本 20 及以上

仿照 GoogleMap 撸一个

写个练手的小 demo,在 Flutter 层显示 AndroidTextView,至于功能,就做一个设置 Text 内容和文字大小

实现 Flutter 端的代码
const _textType = "com.demo.plugin/textview"; // 用于注册 AndroidView
const _textMethodChannel = "com.demo.plugin/textview_"; // 用于注册 MethodChannel

// 参考 GoogleMap,通过 controller 来实现方法的交互,view 只负责展示
class TextController {
  final MethodChannel _channel;
  // 在构造函数注册 MethodChannel
  TextController(int _id) : _channel = MethodChannel('$_textMethodChannel$_id');

  // 设置文字方法
  Future<void> setText(String text) {
    assert(text != null);
    return _channel.invokeMethod("text#setText", text);
  }

  // 设置文字大小方法
  Future<void> setTextSize(double size) {
    assert(size != null);
    return _channel.invokeMethod("text#setTextSize", size);
  }
}

// 用于给展示的 view 设置 controller
typedef void TextViewCreateWatcher(TextController controller);

// 只用于展示
class TextView extends StatefulWidget {
  final TextViewCreateWatcher watcher;

  TextView(this.watcher, {Key key}) : super(key: key);

  @override
  _TextViewState createState() => _TextViewState();
}

class _TextViewState extends State<TextView> {
  @override
  Widget build(BuildContext context) {
    // 目前只做 AndroidView, UiKitView 有兴趣可自行搞定
    return defaultTargetPlatform == TargetPlatform.android  
        ? AndroidView(
            viewType: _textType,
            onPlatformViewCreated: _onPlatformViewCreated,
          )
        : Text('$defaultTargetPlatform not support TextView yet');
  }

  _onPlatformViewCreated(int id) => widget.watcher(TextController(id));
}
实现 Android 端的代码
// 需要同 flutter 端一致
private const val TextType = "com.demo.plugin/textview"
private const val TextChannel = "com.demo.plugin/textview_"

// 展示的 PlatformView
class FlutterTextView(context: Context?, messenger: BinaryMessenger, id: Int)
    : PlatformView, MethodCallHandler {

    private val textView = TextView(context).apply { gravity = Gravity.CENTER }
    private val channel = MethodChannel(messenger, "$TextChannel$id")

    init {
        channel.setMethodCallHandler(this) // 注册交互的 MethodChannel
    }

    override fun getView(): View = textView // 最终返回的为 textView 实例

    override fun dispose() {}  // textview 无内存泄漏情况,所以该方法可空

    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "text#setText" -> {
                textView.text = call.arguments?.toString() ?: ""
                result.success(null)
            }

            "text#setTextSize" -> {
                // dart 的 double 直接转成 Float 会出错,通过 String 类型来过渡下即可
                textView.textSize = "${call.arguments ?: 12}".toFloat() 
                result.success(null)
            }

            else -> result.notImplemented()
        }
    }
}

// 定义完 PlatformView,则可以实现 PlatformViewFactory
class TextViewFactory(private val messenger: BinaryMessenger)
    : PlatformViewFactory(StandardMessageCodec.INSTANCE) {

    override fun create(context: Context?, id: Int, `object`: Any?):
            PlatformView = FlutterTextView(context, messenger, id)  // 返回 PlatformView 即可
}

// 注册该 view
class ViewPlugin {
    companion object {
        @JvmStatic
        fun registerWith(registrar: Registrar) {
            registrar.platformViewRegistry()
                    .registerViewFactory(TextType, TextViewFactory(registrar.messenger()))
        }
    }
}
调用 AndroidView
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('AndroidView'),
      ),
      body: TextView((controller) {
        controller.setText("Hello Wrold!!");
        controller.setTextSize(50.0);
      }),
    );
  }
}

最终将 Android 端的 TextView 显示到 Flutter 层,效果图就不贴了。当然了,这个例子没有一点实用性,只是作为一个简单的例子而已,当遇到 Flutter 缺少原生需要的 View 时候,则可以通过该方法来实现,使用时候注意点参考上面提到的~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345