Flutter 與 Android 的互動

kukyxs發表於2019-05-13

該文已授權公眾號 「碼個蛋」,轉載請指明出處

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 例項。

最後貼上 demo 的地址:ContactDemo

需要了解外掛的寫法也可以直接檢視官方提供的檢測電量外掛:Flutter Battery Plugin

相關文章