26 How to Account for Platform Specific Implementations in Dart for Android I Os I

26 How to Account for Platform-Specific Implementations in Dart for Android-iOS I #

Hello, I’m Chen Hang.

In the previous article, I introduced three ways to achieve data persistence in Flutter, namely files, SharedPreferences, and databases.

Files are suitable for persisting string or binary data. Depending on the access frequency, we can decide to store them in the temporary directory or the document directory. SharedPreferences, on the other hand, are suitable for storing small key-value pairs and can handle lightweight configuration caching scenarios. Databases are suitable for frequently changing, structured object storage and can easily handle data CRUD operations.

Leveraging deep customization and optimization with Skia, Flutter provides us with a lot of control and support for rendering, enabling us to achieve absolute cross-platform consistency in application layer rendering. However, apart from visual display and corresponding interactive logic processing at the application layer, an application sometimes needs support for underlying capabilities provided by the native operating systems (Android, iOS). For example, the aforementioned data persistence, as well as push notifications and calls to camera hardware.

Since Flutter only takes charge of the application rendering layer, these system-level capabilities cannot be supported within the Flutter framework. On the other hand, Flutter is still a relatively young ecosystem, so there may not be relevant implementations for some mature Java, C++, or Objective-C code libraries in Flutter, such as image processing and audio/video encoding and decoding.

Therefore, in order to address the need for accessing native system-level capabilities and reusing related code libraries, Flutter provides developers with a lightweight solution called method channels in the logic layer. Based on method channels, we can expose the capabilities of native code to Dart in the form of interfaces, enabling Dart code to interact with native code just like calling a regular Dart API.

Next, I will explain Flutter’s method channel mechanism in detail.

Method Channels #

Flutter is a cross-platform framework that provides a standardized solution to shield developers from differences in operating systems. However, Flutter is not an operating system, so in certain specific scenarios (such as push notifications, Bluetooth, and camera hardware calls), it is also necessary to have the ability to directly access the underlying native code of the system. Therefore, Flutter provides a flexible and lightweight mechanism to achieve communication between Dart and native code through method channel messaging, and the method channel is used to transmit communication messages.

A typical method call process is similar to a network call, where Flutter, as the client, sends a method call request to the native code host, which serves as the server, through a method channel. After the native code host receives the method call message, it uses platform-specific APIs to process the request initiated by Flutter, and finally sends the processed result back to Flutter through the method channel. The process is shown in the following diagram:

Figure 1 Method Channel Diagram

From the diagram above, we can see that the handling and response of method call requests are registered through FlutterView in Android and FlutterViewController in iOS. FlutterView and FlutterViewController provide a canvas for Flutter, which is built on Skia and can achieve the visual effects required by the entire application through drawing. Therefore, they are not only containers for Flutter applications but also the entry points for Flutter applications, making them the most suitable places to register method call requests.

Next, I will demonstrate how to use method channels to interact with native code through an example.

Example of Using Method Channels #

In actual business scenarios, it is a common requirement to prompt users to rate and review an application by redirecting them to the app store (App Store for iOS and various app stores for Android). As Flutter does not provide an interface for such functionality, and the redirection methods differ between Android and iOS, we need to implement this functionality separately on each platform and expose it to Dart through relevant interfaces.

Let’s first see how Flutter, as the client, can make a method call.

How does Flutter make a method call? #

First, we need to define a unique string identifier to create a named channel. Then, using this channel, Flutter initiates a method call by specifying the method name as “openAppMarket”.

As you can see, this is similar to calling a method on a Dart object. Since method calls are asynchronous, we need to use non-blocking (or register callbacks) to wait for the native code to respond.

// Declare the MethodChannel
const platform = MethodChannel('samples.chenhang/utils');

// Handle the button click
handleButtonClick() async {
  int result;
  // Exception handling
  try {
    // Asynchronously wait for the method channel call result
    result = await platform.invokeMethod('openAppMarket');
  } catch (e) {
    result = -1;
  }
  print("Result: $result");
}

Note that, similar to network calls, method calls can fail (for example, if Flutter initiates an API call that is not supported by the native code or if there is an error during the call). Therefore, we wrap the method call statement with try-catch.

Now that we have implemented the caller side, let’s move on to implementing the method call response in the native code host. Since we need to support both Android and iOS platforms, we need to implement the corresponding interfaces on each platform.

Implementing the Method Call Response in the Native Code #

First, let’s take a look at the implementation on the Android side. As mentioned in the last section, the method call handling and response on the Android platform are implemented in the Flutter application’s entry point, specifically in the MainActivity inside the FlutterView. Therefore, we need to open the Flutter Android host app, find the MainActivity.java file, and add the relevant logic there.

Since both the caller and the responder communicate through a named channel, we need to create a MethodChannel in the onCreate method with the same channel name used by the Flutter caller. We also need to set a method handling callback inside it, respond to the openAppMarket method by opening the app store Intent. Similarly, considering that opening the app store can fail, we also need to add try-catch to catch any possible exceptions:

protected void onCreate(Bundle savedInstanceState) {
  ...
  // Create a method channel with the same identifier as the caller
  new MethodChannel(getFlutterView(), "samples.chenhang/utils").setMethodCallHandler(
   // Set the method handling callback
    new MethodCallHandler() {
      // Respond to the method request
      @Override
      public void onMethodCall(MethodCall call, Result result) {
        // Check if the method name is supported
        if(call.method.equals("openAppMarket")) {
          try {
            // App store URI
            Uri uri = Uri.parse("market://details?id=com.hangchen.example.flutter_module_page.host");
            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            // Open the app store
            activity.startActivity(intent);
            // Return the processing result
            result.success(0);
          } catch (Exception e) {
            // Exception occurred while opening the app store
            result.error("UNAVAILABLE", "App store not installed", null);
          }
        } else {
          // Method name not supported
          result.notImplemented();
        }
      }
    });
}

Now, the Android side of the method call response is complete. Next, let’s take a look at how to implement the method call response on the iOS side.

On the iOS platform, the method call handling and response are implemented in the Flutter application’s entry point, specifically in the AppDelegate inside the rootViewController (i.e., FlutterViewController). Therefore, we need to open the Flutter iOS host app, find the AppDelegate.m file, and add the relevant logic there.

Similar to the Android method call registration, we need to create a MethodChannel with the same channel name used by the Flutter caller in the didFinishLaunchingWithOptions: method. Inside it, we set a method handling callback, respond to the openAppMarket method by opening the app store using a URL:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  // Create a named method channel
  FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"samples.chenhang/utils" binaryMessenger:(FlutterViewController *)self.window.rootViewController];
  // Register the method call handling callback in the method channel
  [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
    // Check if the method name matches
    if ([@"openAppMarket" isEqualToString:call.method]) {
      // Open App Store (in this example, we open the URL for WeChat)
      [[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"itms-apps://itunes.apple.com/xy/app/foo/id414478124"]];
      // Return the method processing result
      result(@0);
    } else {
      // Method not found
      result(FlutterMethodNotImplemented);
    }
  }];
  ...
}

Now, the iOS side of the method call response is complete.

Now, we can use the openAppMarket method in the Flutter application to open the app store provided by different operating systems.

It’s worth noting that, when returning the processing result to Flutter after the native code has finished processing, we used three different data types in Dart, Android, and iOS: The Android side returns java.lang.Integer, the iOS side returns NSNumber, and the Dart side receives the returned result as an int. Why is this?

This is because when using method channels for method calls, Flutter uses the StandardMessageCodec to perform binary serialization similar to JSON on the information transmitted through the channel to standardize data transmission behavior. Thus, when we send or receive data, the data is automatically serialized and deserialized according to the rules defined by each platform. With this understanding, you should have a deeper impression of method channel technology, which is similar to network calls.

For the example mentioned above, the return values of type java.lang.Integer or NSNumber are first serialized into binary format data and transmitted through the channel. Then, when this data is passed to Flutter, it is deserialized into the int type in the Dart language.

I have summarized the common data type conversions between Android, iOS, and Dart platforms in the table below to help you understand and remember them. Just remember that basic types like null, boolean, integer, string, array, and map can be mixed and used between platforms according to platform-defined rules.

Figure 2 Common Data Type Conversion between Android, iOS, and Dart Platforms

Conclusion #

Well, that’s all for today’s sharing. Let’s summarize the main content.

Method channels solve the problem of reusing native capabilities at the logic layer, enabling Flutter to interact with native code through lightweight asynchronous method calls. A typical call process starts with Flutter initiating a method call request, which is then sent through a method channel specified by a unique identifier to the native code host. The native code host, in turn, registers, responds, and handles the call request by executing the corresponding method implementation. Finally, the execution result is returned to Flutter through 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 follows a single-threaded model, so method call requests are naturally guaranteed to occur on the main thread (Isolate). When handling method call requests, the native code needs to ensure that the callback process executes on the UI thread of the native system (i.e., the main thread of Android and iOS) if it involves asynchronous or non-main thread switching. Otherwise, the application may experience strange bugs or even crashes.

I have packaged the knowledge points covered in today’s sharing on GitHub. You can download it, run it multiple times, and deepen your understanding.

Thought Question #

Finally, I have a thought question for you.

Please expand the method openAppMarket example to support passing the AppID and package name, so that we can redirect to the app market of any app.

Feel free to leave your thoughts in the comments section. I will be waiting for you in the next article! Thank you for listening, and feel free to share this article with more friends to read together.