Flutter and Native Communication

This post is brought to you by TestFairy, a mobile testing platform that helps companies streamline their mobile development process and fix bugs faster.
See how TestFairy can help you at testfairy.com

When we are working with a cross-platform framework, it isn’t possible to guarantee that we will be working only with that framework without any work in the native code. Developers will most likely resort to native either because of features that the framework hasn’t yet provided or because of performance issues that can only be solved in native. Flutter makes it easy to work with native by providing Platform Channel for communicating with native. The advantage of Flutter is that it supports Kotlin and Swift in addition to Java and Objective-C.

Playground

If you need a simple playground to test your ideas instead of a fully featured example app, feel free to checkout this post.

Related Topics

Take a look at these posts to learn about common communication patterns between Dart and native code if you already know what you are looking for.

Setup

Create a new Flutter project and when prompted with the dialog below, choose to include both Kotlin  and Swift support.

In Android Studio / IntelliJ, the Flutter plugin has two menus to make it easier to open the Android Module in Android Studio and iOS Module in Xcode.

Platform Channel

Platform Channel is a channel that allows you to communicate asynchronously with native. By sending a message and receiving a response asynchronously, it is guaranteed that the main thread won’t become blocked and the UI will remain responsive. In addition to String data type messaging, Flutter also supports List, Map, int, bool & double. The serialization and deserialization for these types will happen automatically when you send or receive a message. Platform Channel consists of three parts: MethodChannel class on dart / Flutter, MethodChannel class on Android and FlutterMethodChannel class on iOS.

MethodChannel class is defined inside the ‘services’ package. To add it to our class, we need to import 2 packages:

import 'dart:async';
import 'package:flutter/services.dart';

We need to import the ‘async’ package to support async features in our dart codebase. To define MethodChannel in dart we use:

static const channel = const MethodChannel('testfairy.flutter.io/hello');

MethodChannel requires a channel name, and this name must be the same when defined in Android and in iOS. You can name it  anything, but the best practice is to name it domain_name/channel_name. To send a message to native we use invokeMethod, as below.

final response = await channel.invokeMethod(message, [optional_arguments])
print(response)

invokeMethod is an async method so we must add await and wrap the call inside an async (Future) method. If there is a result that’s sent back from native it will be assigned to the response variable. We can also send arguments, wrapped in a list,  along with a message.

Next, we’ll start deep diving into native communication with Android and iOS, to follow along, you can clone this repo.

Communicating with Android

Open the Android module in Android Studio. The first exciting thing is that Flutter on Android runs on top of a FlutterActivity, which is a regular Android activity.

class MainActivity : FlutterActivity() {
...
}

To add MethodChannel support, create an instance of MethodChannel class with arguments: flutterView as the message sender/receiver and channel name.

val channel = MethodChannel(flutterView, "flutter.testfairy.com/hello")

Next, we add cases of every possible message and do action based on this message.

channel.setMethodCallHandler { methodCall, result ->
    val args = methodCall.arguments as List<*>
    val param = args.first() as String

    when (methodCall.method) {
        "openPage" -> openSecondActivity(param)
        "showDialog" -> showDialog(param, result)
        "request" -> callService(param, result)
        else -> return@setMethodCallHandler
    }
}

In our example, we added a function to start an Android Activity, by calling startActivity and showing a dialog.

private fun openSecondActivity(info: String) {
    startActivity<SecondActivity>("info" to info)
}
private fun showDialog(content: String, channelResult: MethodChannel.Result) {
    MaterialDialog.Builder(this).title("Native Dialog").theme(Theme.LIGHT)
            .content(content)
            .positiveText("Ok")
            .negativeText("Cancel")
            .onPositive { _, _ -> channelResult.success("Ok was clicked") }
            .onNegative { _, _ -> channelResult.success("Cancel was clicked") }
            .show()
}

You will notice, we used Anko DSL for startActivity and MaterialDialog library to show a dialog with Material style, and both were added as a dependency using Gradle. Yes, you can add dependencies in Gradle and use MethodChannel to interact with them via MethodChannel. We also added function to do a network call via Retrofit and send the result back to Flutter.

private fun callService(url: String, channelResult: MethodChannel.Result) {
    ...
    service.getEPLTeams().enqueue(object : Callback<TeamResponse> {
        override fun onFailure(call: Call<TeamResponse>?, t: Throwable?) {
            channelResult.error("FAILURE", "CALL FAILED", t?.localizedMessage)
        }

        override fun onResponse(call: Call<TeamResponse>?, response: Response<TeamResponse>?) {
            channelResult.success(Gson().toJson(response?.body()?.teams))
        }
    })
}

Notes :

  1. Flutter doesn’t provide a default style in the Android module, you must add them manually and set them in the manifest file for native activity that you are creating .
  2. Always cold restart your app by pressing stop and then run if you have made any changes in native because code changes in native can’t be hot reloaded or hot restarted.

 

 

 

 

Setup iOS module with Cocoapod

Before we start diving into the iOS module, when creating a project in Flutter also setup Cocoapods inside the  module. To use it, in the iOS folder, open podfile and pods before end block inside target ‘Runner’. Do not change anything above it as this is  the setup for Cocoapods to embed frameworks created by Flutter or Flutter plugins.

Before you build your XCode project, make sure you have run flutter package get to build/download the required frameworks. When you run your Flutter project on iOS device/simulator, Flutter will automatically run pod install first before building the project.

 

 

Communicating with iOS

Open iOS module in XCode, the first project skeleton that we can see here is only AppDelegate.swift. To get a Flutter View Controller, we need to retrieve it using window?.rootViewController.

let flutterVC = window?.rootViewController as! FlutterViewController

To add MethodChannel, create an instance of FlutterMethodChannel with arguments: channel name and Flutter View Controller as the message sender/receiver.

let channel = FlutterMethodChannel(name: "flutter.testfairy.com/hello", binaryMessenger: flutterVC)

Next, we add cases of every possible message and do action based on this message.

channel.setMethodCallHandler { [unowned self] (methodCall, result) in
    guard let arg = (methodCall.arguments as! [String]).first else { return }

    switch methodCall.method {
    case "openPage":
        self.openSecondPage(param: arg)
    case "showDialog":
        self.openAlert(param: arg, result: result)
    case "request":
        self.callApi(url: arg, result: result)
    default:
        debugPrint(methodCall.method)
        result(methodCall.method)
    }
}

In our example, we added function to present a UINavigationController and sent a message argument to be shown in UIViewController (child of UINavigationController). For showDialog message, we create a UIAlertController with actions and send the result back to Flutter based on an action that was selected.

private func openSecondPage(param: String) {
    let sb = UIStoryboard(name: "Main", bundle: nil)
    let nav = sb.instantiateViewController(withIdentifier: "NavSecond")

    if let vc = nav.childViewControllers.first as? SecondViewController {
        vc.bodyTitle = param
    }

    flutterVC.present(nav, animated: true, completion: nil)
}
private func openAlert(param: String, result: @escaping FlutterResult) {
    let alert = UIAlertController(title: "Native Alert", message: param, preferredStyle: .alert)
    let okAction = UIAlertAction(title: "Ok", style: .default) { (_) in
        result("Ok was pressed")
    }
    let cancelAction = UIAlertAction(title: "Cancel", style: .destructive) { (_) in
        result("Cancel was pressed")
    }
    alert.addAction(cancelAction)
    alert.addAction(okAction)
    flutterVC.present(alert, animated: true, completion: nil)
}

For request message, we make a network request using Alamofire and show progress alert using JGProgressHUD, both libraries were added using Cocoapod.

private func callApi(url: String, result: @escaping FlutterResult) {
    let hud = JGProgressHUD(style: .dark)
    hud.textLabel.text = "Loading"
    hud.show(in: flutterVC.view)

    guard let fullUrl = "\(url)search_all_teams.php?l=English Premier League".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { return }

    Alamofire.request(fullUrl).responseJSON { (response) in
        hud.dismiss()

        if let data = response.result.value {
            let json = JSON(data)
            result(json["teams"].rawString())
        }
    }
}

Note :

  • Some static frameworks seem to have difficulties when integrating with Flutter and iOS Module even though you’ve added use_frameworks! inside podfile.