【Flutter 混合開發(fā)】嵌入原生View-Android

Flutter 混合開發(fā)系列 包含如下:

  • 嵌入原生View-Android
  • 嵌入原生View-IOS
  • 與原生通信-MethodChannel
  • 與原生通信-BasicMessageChannel
  • 與原生通信-EventChannel
  • 添加 Flutter 到 Android Activity
  • 添加 Flutter 到 Android Fragment
  • 添加 Flutter 到 iOS

每個工作日分享一篇纠永,歡迎關(guān)注、點贊及轉(zhuǎn)發(fā)席镀。

AndroidView

建議使用 Android Studio 進行開發(fā)马胧,在 Android Studio 左側(cè) project tab下選中 android 目錄下任意一個文件,右上角會出現(xiàn) Open for Editing in Android Studio

點擊即可打開,打開后 project tab 并不是一個 Android 項目,而是項目中所有 Android 項目蜕琴,包含第三方:

app 目錄是當前項目的 android 目錄,其他則是第三方的 android 目錄宵溅。

App 項目的 java/包名 目錄下創(chuàng)建嵌入 Flutter 中的 Android View凌简,此 View 繼承 PlatformView

class MyFlutterView(context: Context) : PlatformView {
    override fun getView(): View {
        TODO("Not yet implemented")
    }

    override fun dispose() {
        TODO("Not yet implemented")
    }
}
  • getView :返回要嵌入 Flutter 層次結(jié)構(gòu)的Android View
  • dispose:釋放此View時調(diào)用,此方法調(diào)用后 View 不可用恃逻,此方法需要清除所有對象引用雏搂,否則會造成內(nèi)存泄漏藕施。

返回一個簡單的 TextView

class MyFlutterView(context: Context, messenger: BinaryMessenger, viewId: Int, args: Map<String, Any>?) : PlatformView {

    val textView: TextView = TextView(context)

    init {
        textView.text = "我是Android View"
    }

    override fun getView(): View {

        return textView
    }

    override fun dispose() {
        TODO("Not yet implemented")
    }
}
  • messenger:用于消息傳遞,后面介紹 Flutter 與 原生通信時用到此參數(shù)凸郑。
  • viewId:View 生成時會分配一個唯一 ID裳食。
  • args:Flutter 傳遞的初始化參數(shù)。

注冊PlatformView

創(chuàng)建PlatformViewFactory:

class MyFlutterViewFactory(val messenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {

    override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
        val flutterView = MyFlutterView(context, messenger, viewId, args as Map<String, Any>?)
        return flutterView
    }

}

創(chuàng)建 MyPlugin

class MyPlugin : FlutterPlugin {

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        val messenger: BinaryMessenger = binding.binaryMessenger
        binding
                .platformViewRegistry
                .registerViewFactory(
                        "plugins.flutter.io/custom_platform_view", MyFlutterViewFactory(messenger))
    }

    companion object {
        @JvmStatic
        fun registerWith(registrar: PluginRegistry.Registrar) {
            registrar
                    .platformViewRegistry()
                    .registerViewFactory(
                            "plugins.flutter.io/custom_platform_view",
                            MyFlutterViewFactory(registrar.messenger()))
        }
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {

    }
}

記住 plugins.flutter.io/custom_platform_view 芙沥,這個字符串在 Flutter 中需要與其保持一致诲祸。

App 中 MainActivity 中注冊:

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        flutterEngine.plugins.add(MyPlugin())
    }
}

如果是 Flutter Plugin,沒有MainActivity而昨,則在對應(yīng)的 Plugin onAttachedToEngine 和 registerWith 方法修改如下:

public class CustomPlatformViewPlugin : FlutterPlugin,MethodCallHandler {
    /// The MethodChannel that will the communication between Flutter and native Android
    ///
    /// This local reference serves to register the plugin with the Flutter Engine and unregister it
    /// when the Flutter Engine is detached from the Activity
    private lateinit var channel: MethodChannel

    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "custom_platform_view")
        channel.setMethodCallHandler(this)

        val messenger: BinaryMessenger = flutterPluginBinding.binaryMessenger
        flutterPluginBinding
                .platformViewRegistry
                .registerViewFactory(
                        "plugins.flutter.io/custom_platform_view", MyFlutterViewFactory(messenger))

    }

    // This static function is optional and equivalent to onAttachedToEngine. It supports the old
    // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
    // plugin registration via this function while apps migrate to use the new Android APIs
    // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
    //
    // It is encouraged to share logic between onAttachedToEngine and registerWith to keep
    // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
    // depending on the user's project. onAttachedToEngine or registerWith must both be defined
    // in the same class.
    companion object {
        @JvmStatic
        fun registerWith(registrar: Registrar) {
            val channel = MethodChannel(registrar.messenger(), "custom_platform_view")
            channel.setMethodCallHandler(CustomPlatformViewPlugin())

            registrar
                    .platformViewRegistry()
                    .registerViewFactory(
                            "plugins.flutter.io/custom_platform_view",
                            MyFlutterViewFactory(registrar.messenger()))
        }
    }

    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        if (call.method == "getPlatformVersion") {
            result.success("Android ${android.os.Build.VERSION.RELEASE}")
        } else {
            result.notImplemented()
        }
    }

    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null)
    }
}

嵌入Flutter

在 Flutter 中調(diào)用

class PlatformViewDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Widget platformView(){
      if(defaultTargetPlatform == TargetPlatform.android){
        return AndroidView(
          viewType: 'plugins.flutter.io/custom_platform_view',
        );
      }
    }
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: platformView(),
      ),
    );
  }
}

上面嵌入的是 Android View救氯,因此通過 defaultTargetPlatform == TargetPlatform.android 判斷當前平臺加載,在 Android 上運行效果:

設(shè)置初始化參數(shù)

Flutter 端修改如下:

AndroidView(
          viewType: 'plugins.flutter.io/custom_platform_view',
          creationParams: {'text': 'Flutter傳給AndroidTextView的參數(shù)'},
          creationParamsCodec: StandardMessageCodec(),
        )
  • creationParams :傳遞的參數(shù)配紫,插件可以將此參數(shù)傳遞給 AndroidView 的構(gòu)造函數(shù)径密。
  • creationParamsCodec :將 creationParams 編碼后再發(fā)送給平臺側(cè),它應(yīng)該與傳遞給構(gòu)造函數(shù)的編解碼器匹配躺孝。值的范圍:
    • StandardMessageCodec
    • JSONMessageCodec
    • StringCodec
    • BinaryCodec

修改 MyFlutterView :

class MyFlutterView(context: Context, messenger: BinaryMessenger, viewId: Int, args: Map<String, Any>?) : PlatformView {

    val textView: TextView = TextView(context)

    init {
        args?.also {
            textView.text = it["text"] as String
        }
    }

    override fun getView(): View {

        return textView
    }

    override fun dispose() {
        TODO("Not yet implemented")
    }
}

最終效果:

Flutter 向 Android View 發(fā)送消息

修改 Flutter 端,創(chuàng)建 MethodChannel 用于通信:

class PlatformViewDemo extends StatefulWidget {
  @override
  _PlatformViewDemoState createState() => _PlatformViewDemoState();
}

class _PlatformViewDemoState extends State<PlatformViewDemo> {
  static const platform =
      const MethodChannel('com.flutter.guide.MyFlutterView');

  @override
  Widget build(BuildContext context) {
    Widget platformView() {
      if (defaultTargetPlatform == TargetPlatform.android) {
        return AndroidView(
          viewType: 'plugins.flutter.io/custom_platform_view',
          creationParams: {'text': 'Flutter傳給AndroidTextView的參數(shù)'},
          creationParamsCodec: StandardMessageCodec(),
        );
      }
    }

    return Scaffold(
      appBar: AppBar(),
      body: Column(children: [
        RaisedButton(
          child: Text('傳遞參數(shù)給原生View'),
          onPressed: () {
            platform.invokeMethod('setText', {'name': 'laomeng', 'age': 18});
          },
        ),
        Expanded(child: platformView()),
      ]),
    );
  }
}

在 原生View 中也創(chuàng)建一個 MethodChannel 用于通信:

class MyFlutterView(context: Context, messenger: BinaryMessenger, viewId: Int, args: Map<String, Any>?) : PlatformView, MethodChannel.MethodCallHandler {

    val textView: TextView = TextView(context)
    private var methodChannel: MethodChannel

    init {
        args?.also {
            textView.text = it["text"] as String
        }
        methodChannel = MethodChannel(messenger, "com.flutter.guide.MyFlutterView")
        methodChannel.setMethodCallHandler(this)
    }

    override fun getView(): View {

        return textView
    }

    override fun dispose() {
        methodChannel.setMethodCallHandler(null)
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        if (call.method == "setText") {
            val name = call.argument("name") as String?
            val age = call.argument("age") as Int?

            textView.text = "hello,$name,年齡:$age"
        } else {
            result.notImplemented()
        }
    }
}

Flutter 向 Android View 獲取消息

與上面發(fā)送信息不同的是底桂,F(xiàn)lutter 向原生請求數(shù)據(jù)植袍,原生返回數(shù)據(jù)到 Flutter 端,修改 MyFlutterView onMethodCall

override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
    if (call.method == "setText") {
        val name = call.argument("name") as String?
        val age = call.argument("age") as Int?
        textView.text = "hello,$name,年齡:$age"
    } else if (call.method == "getData") {
        val name = call.argument("name") as String?
        val age = call.argument("age") as Int?

        var map = mapOf("name" to "hello,$name",
                "age" to "$age"
        )
        result.success(map)
    } else {
        result.notImplemented()
    }
}

result.success(map) 是返回的數(shù)據(jù)籽懦。

Flutter 端接收數(shù)據(jù):

var _data = '獲取數(shù)據(jù)';

RaisedButton(
  child: Text('$_data'),
  onPressed: () async {
    var result = await platform
        .invokeMethod('getData', {'name': 'laomeng', 'age': 18});
    setState(() {
      _data = '${result['name']},${result['age']}';
    });
  },
),

解決多個原生View通信沖突問題

當然頁面有3個原生View于个,

class PlatformViewDemo extends StatefulWidget {
  @override
  _PlatformViewDemoState createState() => _PlatformViewDemoState();
}

class _PlatformViewDemoState extends State<PlatformViewDemo> {
  static const platform =
      const MethodChannel('com.flutter.guide.MyFlutterView');

  var _data = '獲取數(shù)據(jù)';

  @override
  Widget build(BuildContext context) {
    Widget platformView() {
      if (defaultTargetPlatform == TargetPlatform.android) {
        return AndroidView(
          viewType: 'plugins.flutter.io/custom_platform_view',
          creationParams: {'text': 'Flutter傳給AndroidTextView的參數(shù)'},
          creationParamsCodec: StandardMessageCodec(),
        );
      }
    }

    return Scaffold(
      appBar: AppBar(),
      body: Column(children: [
        Row(
          children: [
            RaisedButton(
              child: Text('傳遞參數(shù)給原生View'),
              onPressed: () {
                platform
                    .invokeMethod('setText', {'name': 'laomeng', 'age': 18});
              },
            ),
            RaisedButton(
              child: Text('$_data'),
              onPressed: () async {
                var result = await platform
                    .invokeMethod('getData', {'name': 'laomeng', 'age': 18});
                setState(() {
                  _data = '${result['name']},${result['age']}';
                });
              },
            ),
          ],
        ),
        Expanded(child: Container(color: Colors.red, child: platformView())),
        Expanded(child: Container(color: Colors.blue, child: platformView())),
        Expanded(child: Container(color: Colors.yellow, child: platformView())),
      ]),
    );
  }
}

此時點擊 傳遞參數(shù)給原生View 按鈕哪個View會改變內(nèi)容,實際上只有最后一個會改變暮顺。

如何改變指定View的內(nèi)容厅篓?重點是 MethodChannel,只需修改上面3個通道的名稱不相同即可:

  • 第一種方法:將一個唯一 id 通過初始化參數(shù)傳遞給原生 View捶码,原生 View使用這個id 構(gòu)建不同名稱的 MethodChannel羽氮。
  • 第二種方法(推薦):原生 View 生成時,系統(tǒng)會為其生成唯一id:viewId惫恼,使用 viewId 構(gòu)建不同名稱的 MethodChannel档押。

原生 View 使用 viewId 構(gòu)建不同名稱的 MethodChannel

class MyFlutterView(context: Context, messenger: BinaryMessenger, viewId: Int, args: Map<String, Any>?) : PlatformView, MethodChannel.MethodCallHandler {

    val textView: TextView = TextView(context)
    private var methodChannel: MethodChannel

    init {
        args?.also {
            textView.text = it["text"] as String
        }
        methodChannel = MethodChannel(messenger, "com.flutter.guide.MyFlutterView_$viewId")
        methodChannel.setMethodCallHandler(this)
    }
  ...
}

Flutter 端為每一個原生 View 創(chuàng)建不同的MethodChannel

var platforms = [];

AndroidView(
  viewType: 'plugins.flutter.io/custom_platform_view',
  onPlatformViewCreated: (viewId) {
    print('viewId:$viewId');
    platforms
        .add(MethodChannel('com.flutter.guide.MyFlutterView_$viewId'));
  },
  creationParams: {'text': 'Flutter傳給AndroidTextView的參數(shù)'},
  creationParamsCodec: StandardMessageCodec(),
)

給第一個發(fā)送消息:

platforms[0]
    .invokeMethod('setText', {'name': 'laomeng', 'age': 18});

交流

老孟Flutter博客(330個控件用法+實戰(zhàn)入門系列文章):http://laomengit.com

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市祈纯,隨后出現(xiàn)的幾起案子令宿,更是在濱河造成了極大的恐慌,老刑警劉巖腕窥,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粒没,死亡現(xiàn)場離奇詭異,居然都是意外死亡簇爆,警方通過查閱死者的電腦和手機癞松,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門倾贰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人拦惋,你說我怎么就攤上這事匆浙。” “怎么了厕妖?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵首尼,是天一觀的道長。 經(jīng)常有香客問我言秸,道長软能,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任举畸,我火速辦了婚禮查排,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抄沮。我一直安慰自己跋核,他們只是感情好,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布叛买。 她就那樣靜靜地躺著砂代,像睡著了一般。 火紅的嫁衣襯著肌膚如雪率挣。 梳的紋絲不亂的頭發(fā)上刻伊,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音椒功,去河邊找鬼捶箱。 笑死,一個胖子當著我的面吹牛动漾,可吹牛的內(nèi)容都是我干的丁屎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼谦炬,長吁一口氣:“原來是場噩夢啊……” “哼悦屏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起键思,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤础爬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后吼鳞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體看蚜,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年赔桌,在試婚紗的時候發(fā)現(xiàn)自己被綠了供炎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渴逻。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖音诫,靈堂內(nèi)的尸體忽然破棺而出惨奕,到底是詐尸還是另有隱情,我是刑警寧澤竭钝,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布梨撞,位于F島的核電站,受9級特大地震影響香罐,放射性物質(zhì)發(fā)生泄漏卧波。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一庇茫、第九天 我趴在偏房一處隱蔽的房頂上張望港粱。 院中可真熱鬧,春花似錦旦签、人聲如沸查坪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咪惠。三九已至,卻和暖如春淋淀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背覆醇。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工朵纷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人永脓。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓袍辞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親常摧。 傳聞我的和親對象是個殘疾皇子搅吁,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355