Flutter 與 Android 的交互

該文已受權公衆號 「碼個蛋」,轉載請指明出處php

Flutter 說到底只是一個 UI 框架,不少功能都須要經過原生的 Api 來實現,那麼就會涉及到 Flutter 和 Native 的交互,由於本人不懂 iOS 開發,因此只能講下 Flutter 同 Android 的交互。java

Android 項目配置 Flutter 依賴

既然是互相交互,那麼須要準備一個 Android 項目。接着就須要建立 flutter module,讓 Android 項目依賴,建立的方法能夠參考官網 Flutter Wiki,雖然是官網提供的方法,可是徹底按照這個步驟來,仍是會有坑的,這邊就慢慢一步步解決坑。android

若是你用的是 Android Studio 進行開發的話,直接打開底部的 Terminal,直接建立 flutter module 依賴git

flutter create -t module flutter_native_contact 至於 module 名能夠隨意填寫,module 建立完後結構大概是這樣的github

flutter module.png

接着切換到 module 下的 .android 文件夾,接着有坑來了,官網提供的方法是 ./gradlew flutter:assembleDebug 可能會提示命令不存在,那麼直接經過 gradlew flutter:assembleDebug 來運行,等它自動跑完後,打開根目錄下的 settings.gradle 文件,加入官網提供的 gradle 代碼api

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

你覺得這裏沒坑,真是圖樣圖森破,沒坑是不可能的,編譯器大爺可能會給你甩這麼個錯誤markdown

error.png

很明顯能夠看出是找不到咱們的文件,因此把文件名路徑給補全app

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 的依賴就完成了。這個過程告訴咱們一個什麼道理呢?*不要覺得官網的都對,官網講的也不是徹底可信的,時不時給你來個坑就能卡你老半天。less

原生界面加載 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 widget 同樣,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 時候,則能夠經過該方法來實現,使用時候注意點參考上面提到的~

相關文章
相關標籤/搜索