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閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370