Embedded Android native View in fluent

It is recommended to use Android Studio for development. Select any file under the android directory under the project tab on the left of Android Studio, and "Open for Editing in Android Studio" will appear in the upper right corner,

Click to open it. After opening, the project tab is not an Android project, but all Android projects in the project, including third parties:

The app directory is the android directory of the current project, and others are the android directories of third parties.

Create an Android View embedded in the shuttle under the java / package name directory of the "App" project. This View inherits the "PlatformView":

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

    override fun dispose() {
        TODO("Not yet implemented")
    }
}
  • "getView": returns the Android View to be embedded in the fluent hierarchy
  • "dispose": called when releasing this View. After this method is called, the View is unavailable. This method needs to clear all object references, otherwise it will cause memory leakage.

Return to a simple TextView:

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

    val textView: TextView = TextView(context)

    init {
        textView.text = "I am Android View"
    }

    override fun getView(): View {

        return textView
    }

    override fun dispose() {
        TODO("Not yet implemented")
    }
}
    
  • "messenger": used for message passing. This parameter will be used when communicating with native Flutter later.
  • "viewId": a unique ID will be assigned when the View is generated.
  • "args": initialization parameters passed by fluent.

Register PlatformView

To create a 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
    }

}
    

Create 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) {

    }
}
    

Remember "plugins. Fluent. IO / custom_platform_view". This string should be consistent with it in fluent.

Register in "App MainActivity":

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

    

If it is a shuttle plugin and there is no "MainActivity", modify the corresponding "Plugin onAttachedToEngine and registerWith" methods as follows:

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)
    }
}

    

Embedded Flutter

Call in Flutter

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(),
      ),
    );
  }
}
    

The Android View is embedded above. Therefore, judge the current platform loading through "defaultTargetPlatform == TargetPlatform.android" and the running effect on Android:

Set initialization parameters

The Fletter end is modified as follows:

AndroidView(
          viewType: 'plugins.flutter.io/custom_platform_view',
          creationParams: {'text': 'Flutter Pass to AndroidTextView Parameters of'},
          creationParamsCodec: StandardMessageCodec(),
        )

    
  • "creationParams": the passed parameter. The plug-in can pass this parameter to the constructor of AndroidView.
  • creationParamsCodec : encode creationParams and send it to the platform side. It should match the codec passed to the constructor. Range of values:
    • StandardMessageCodec
    • JSONMessageCodec
    • StringCodec
    • BinaryCodec

Modify 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")
    }
}

    

Final effect:

Flutter sends a message to Android View

Modify the fluent end and create a "MethodChannel" for communication:

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 Pass to AndroidTextView Parameters of'},
          creationParamsCodec: StandardMessageCodec(),
        );
      }
    }

    return Scaffold(
      appBar: AppBar(),
      body: Column(children: [
        RaisedButton(
          child: Text('Pass parameters to native View'),
          onPressed: () {
            platform.invokeMethod('setText', {'name': 'laomeng', 'age': 18});
          },
        ),
        Expanded(child: platformView()),
      ]),
    );
  }
}
    

A "MethodChannel" is also created in the native View for communication:

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: $age"
        } else {
            result.notImplemented()
        }
    }
}

Fluent gets messages from Android View

Different from the information sent above, the Flutter requests data from the native, returns the data to the Flutter, and modifies "myfluterview 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: $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) is the returned data.

Data received at the Fletter end:

var _data = 'get data';

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

    

Resolve multiple native View communication conflicts

Of course, the page has three native views,

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

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

  var _data = 'get data';

  @override
  Widget build(BuildContext context) {
    Widget platformView() {
      if (defaultTargetPlatform == TargetPlatform.android) {
        return AndroidView(
          viewType: 'plugins.flutter.io/custom_platform_view',
          creationParams: {'text': 'Flutter Pass to AndroidTextView Parameters of'},
          creationParamsCodec: StandardMessageCodec(),
        );
      }
    }

    return Scaffold(
      appBar: AppBar(),
      body: Column(children: [
        Row(
          children: [
            RaisedButton(
              child: Text('Pass parameters to native 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())),
      ]),
    );
  }
}
    

At this time, click the "pass parameters to native View" button, which View will change the content. In fact, only the last one will change.

How to change the content of the specified View? The key point is "MethodChannel". Just modify the names of the above three channels to be different:

  • The first method: pass a unique id to the native View through initialization parameters. The native View uses this id to build "methodchannels" with different names.
  • The second method (recommended): when a native View is generated, the system will generate a unique id: viewId, and use viewId to build a "MethodChannel" with different names.

The native View uses viewId to build "methodchannels" with different names:

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)
    }
  ...
}
    

The fluent side creates a different "MethodChannel" for each native View:

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 Pass to AndroidTextView Parameters of'},
  creationParamsCodec: StandardMessageCodec(),
)

    

Send a message to the first:

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


Posted on Fri, 26 Nov 2021 04:11:31 -0500 by xcoderx