Based on deep customization and optimization with Skia, Flutter provides us with a lot of control and support for rendering, enabling absolute cross-platform application layer rendering consistency. However, for an application, in addition to the visual display of the application layer and the corresponding interactive logic processing, sometimes it also needs the support of the underlying capabilities provided by the native operating system (Android, iOS). For example, data persistence mentioned earlier, as well as push, camera hardware calls, etc.

Since Flutter only takes over the application rendering layer, these underlying system capabilities cannot be supported within the Flutter framework; On the other hand, Flutter is a relatively young ecosystem, so some of the Java, C++, or Objective-C libraries that are relatively mature in native development, such as image processing, audio and video codec, may not be implemented in Flutter.

There are two main reasons for adding native features to the Flutter project

  • 1. Communication between Flutter and the native platform
  • Embed native pages in Flutter pages

1. Communication between Flutter and the native platform

Flutter provides developers with a lightweight solution to the problem of invoking the underlying capabilities of native systems and reusing the associated code base. Flutter provides a logical layer Method Channel mechanism. Based on the method channel, we can expose the capabilities of native code to Dart in the form of interfaces that allow the Dart code to interact with native code as if it were calling a normal Dart API.

When a native method is invoked in a Flutter, the call information is passed to the native through the platform channel. The native can perform the specified operation only after receiving the call information. If data needs to be returned, the native will pass the data to the Flutter through the platform channel. It is worth noting that messaging is asynchronous, which ensures that the user interface does not hang during messaging.

Platform communication in three ways

There are three ways for Flutter to communicate with Native terminal:

  • MethodChannel: The Flutter and the Native can call each other, and the result can be returned after the call. The Native can call the Flutter actively, and the Flutter can call the result actively, which is two-way communication. This is the most common method, and Native calls need to be executed in the main thread.
  • BasicMessageChannel: Used to encode and decode messages using a specified codec. It is bidirectional communication and can be actively invoked by either the Native end or the Flutter.
  • EventChannel: Communication for Event Streams. The Native end actively sends data to

Common data type conversions between Android, iOS, and Dart platforms

The platform channel uses standard message codec to encode and decode messages, which can efficiently serialize and deserialize messages in binary. Due to the differences in data types between Dart and native platforms, the mapping between data types is listed below.

When values are sent and received, the serialization and deserialization of those values in the message takes place automatically.

How to obtain platform information

Dart provides a global variable defaultTargetPlatform to get information about the current application platform. DefaultTargetPlatform is defined in platform.dart. The definition is as follows:

enum TargetPlatform {
  android,
  fuchsia,
  iOS,
  linux,
  macOS,
  windows,
}
Copy the code

You can see that Flutter currently only supports these three platforms. We can determine the platform by the following code

If (defaultTargetPlatform == targetPlatform. android){if(defaultTargetPlatform == targetPlatform. android){ Do something}else if(defaultTargetPlatform == targetPlatform. iOS){Copy the code

Use the sample

Add us to the dictionary that a Flutter passes to the native {” Flutter “:” I am a Flutter “}. The native passes an array to the Flutter [1,2,3].

How does Flutter implement a method call request

First, we need to determine a unique string identifier to construct a named channel; On this channel, Flutter then initiates a method call request by specifying the method name flutter_postData.

As you can see, this is exactly how we normally call a Dart object. Because the method invocation process is asynchronous, we need to use non-blocking (or register callbacks) to wait for the native code to respond.

// declare MethodChannel const platform = MethodChannel('flutter_postData'); // Handle the button onPressed: () async{List result; Try {result = await platform.invokeMethod('flutter_postData',{"flutter":" I am flutter"}); }catch(e){ result = []; } print(result.toString()); },Copy the code

How is the method call response implemented on iOS

First open the iOS section of the Flutter app in Xcode:

On iOS, method calls are handled and responded to inside the entry to the Flutter application, the rootViewController (FlutterViewController) in Applegate. Therefore, open the iOS host App of Flutter, find the appdelegate. m file, and add the relevant logic.

@UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: Self) / / create a named method of channel let methodChannel = FlutterMethodChannel. Init (name: "flutter_postData", binaryMessenger: self.window.rootViewController as! FlutterBinaryMessenger) / / to treatment channel registration method calls the callback methodChannel. SetMethodCallHandler {(call, Result) in if("flutter_postData" == call.method){// Print (call.arguments?? {}) / / to give value to flutter DispatchQueue. Main. Async {result ([" 1 ", "2", "3")); } } } return super.application(application, didFinishLaunchingWithOptions: launchOptions) } }Copy the code

Click the button to print

How to implement method call response on Android

Start by opening the Android part of your Flutter application in Android Studio:

On Android, method calls are handled and responded to in the entry of the Flutter application, namely the FlutterView in the MainActivity. Therefore, we need to open the Android host App of Flutter. Find the mainActivity.java file and add the relevant logic to it.

Next, create a MethodChannel in onCreate and set up a MethodCallHandler. Be sure to use the same name as the channel name used in the Flutter client.

import android.os.Bundle; import io.flutter.Log; import io.flutter.app.FlutterActivity; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; public class MainActivity extends FlutterActivity { private static final String CHANNEL = "flutter_postData"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler( new MethodCallHandler() { @Override public void onMethodCall(MethodCall call, Result Result){// TODO if(call.method.equals("flutter_postData")){// Print the value from flutter log.e (call.arguments); / / to give value to flutter result. Success (new String [] {" 1 ", "2", "3"}); }}}); }}Copy the code

conclusion

The request arrives at the native code host via a method channel specified by a unique identifier. The native code host implements, responds to, and processes the call request by registering the corresponding method. Finally, the execution result is transmitted back to the Flutter via a message channel.

It is important to note that method channels are not thread-safe. This means that all interface calls between native code and Flutter must occur on the main thread. Flutter is a single-threaded model, so it is natural to ensure that method call requests occur on the main thread. Native code handling method call requests that involve asynchronous or off-main thread switching needs to make sure that the callback is executed in the native system’s UI thread (i.e., the main thread of Android and iOS), otherwise the application may experience strange bugs or even Crash.

2. Native views are nested in the Flutter view

So what does it take to build a complex App? We first decompose capability and rendering into four dimensions according to the four-quadrant analysis method to analyze what is needed to build a complex App.

After analysis, we finally found that there were so many knowledge points to cover to build an App that Flutter and method channels could only handle application layer rendering, application layer capability and underlying capability. For scenes involving underlying rendering, such as browser, camera, map, and native custom view, Flutter and method channel could only handle application layer rendering, application layer capability and underlying capability. It is obviously not practical to develop a new one on Flutter yourself.

In this case, using a mixed view seems like a good choice. We can reserve a blank area in the Widget tree of Flutter in advance and embed a native view matching the blank area in the Flutter’s artboard (i.e. FlutterView and FlutterViewController). You can get the visuals you want.

However, this approach is extremely inelegant because the embedded native view is not in the Flutter rendering hierarchy and requires a lot of adaptation work on both the Flutter side and the native side to achieve a normal user interaction experience.

Fortunately, Flutter provides the concept of a Platform View. It provides a way for developers to embed native systems (Android and iOS) views into Flutter and add them to the render tree of Flutter to achieve an interactive experience consistent with Flutter.

This way, through the platform view, we can wrap a native control as a Flutter control and embed it in a Flutter page just like a normal Widget

Method of use

  • 1. First, the client of Flutter initiates the creation request of the native view by passing a view identifier to the native view wrapper class of Flutter (UIKitView and AndroidView on iOS and Android platforms respectively).
  • 2. The native code side then hands over the creation of the corresponding native view to the PlatformViewFactory.
  • 3. Finally, register the view identifier with the platform view factory on the native code side, so that the view creation request initiated by Flutter can directly find the corresponding view creation factory.

How does Flutter implement native view interface calls

Class MyFlutterView extends StatelessWidget {@override Widget build(BuildContext Context) MyFlutterView if (defaultTargetPlatform == targetPlatform. android) {return AndroidView(viewType: 'MyFlutterView'); } else {MyFlutterView return UIKitView (viewType: 'MyFlutterView'); }}}Copy the code

Embed native View-ios

  • Create FlutterPlatformView
import Foundation import Flutter class MyFlutterView: NSObject,FlutterPlatformView { let label = UILabel() init(_ frame: CGRect,viewID: Int64,args :Any? ,messenger :FlutterBinaryMessenger) {label.text = "iOS View"} func View () -> UIView {return label}}Copy the code
  • 2, register factory classMyFlutterViewFactory
import Foundation
import Flutter
class MyFlutterViewFactory: NSObject,FlutterPlatformViewFactory {
    
    var messenger:FlutterBinaryMessenger
    
    init(messenger:FlutterBinaryMessenger) {
        self.messenger = messenger
        super.init()
    }
    
    func create(withFrame frame: CGRect, viewIdentifier viewId: Int64, arguments args: Any?) -> FlutterPlatformView {
        return MyFlutterView(frame,viewID: viewId,args: args,messenger: messenger)
    }
    
    func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
        return FlutterStandardMessageCodec.sharedInstance()
    }
}
Copy the code
  • 3. Register in AppDelegate
let registrar:FlutterPluginRegistrar = self.registrar(forPlugin: "plugins.flutter.io/custom_platform_view_plugin")!
let factory = MyFlutterViewFactory(messenger: registrar.messenger())
registrar.register(factory, withId: "MyFlutterView")
Copy the code

Embed native View-Android

  • Create an Android View embedded in Flutter under the App project’s Java/package name directory. This View inherits PlatformView
Class MyFlutterView implements PlatformView {private final TextView TextView; // Cache native view // initialize method, Public MyFlutterView(Context Context, int ID, BinaryMessenger messenger) {textView = new textView (Context); Textview.settext (" I'm an Android View"); } @override public View getView() {return textView; Override public void dispose() {}}Copy the code
  • 2, register factory classMyFlutterViewFactory
Public class MyFlutterViewFactory extends PlatformViewFactory {private final BinaryMessenger; / / initialization method public MyFlutterViewFactory (BinaryMessenger msger) {super (StandardMessageCodec. INSTANCE); messenger = msger; } // Create a native view wrapper class, @override public PlatformView create(Context Context, int id, Object obj) {return new MyFlutterView(Context, id, Object obj); messenger); }}Copy the code
  • 3. Register in MainActivity in App
Registrar registrar = registrarFor("plugins.flutter.io/custom_platform_view_plugin"); MyFlutterViewFactory playerViewFactory = new MyFlutterViewFactory(registrar.messenger()); // Register MyFlutterViewFactory playerViewFactory = new MyFlutterViewFactory(registrar.messenger()); / / factory views generated registrar. PlatformViewRegistry () registerViewFactory (" MyFlutterView, "playerViewFactory); // Register the view factoryCopy the code

conclusion

Because Flutter is completely different from native rendering, there is a significant performance overhead associated with converting different render data. Instantiating multiple native controls on an interface at the same time can have a significant impact on performance, so we should avoid using embedded platform views where Flutter controls can also be used.

In this way, on the one hand, a large amount of adaptive bridge code needs to be written on Android and iOS respectively, which violates the original intention of cross-platform technology and increases subsequent maintenance costs. On the other hand, except for maps, Webviews, cameras, and other special cases involving low-level scenarios, most UI effects that native code can achieve can be implemented with Flutter

Code address: github.com/SunshineBro… This article mainly refers to: www.kancloud.cn/alex_wsc/fl…