Flutter and Native Communication

Back to Blog Home

Flutter and Native Communication

When working with a cross-platform framework, we can’t guarantee that we’ll be working 100% with that framework only without touching the native part. Most likely developers will resort to native either because of features that the framework hasn’t yet provided or because of performance issue that can only be solved in native. Flutter made it easy to work with native, by providing Platform Channel for communicating with native. The advantage of Flutter is it supports Kotlin and Swift besides Java and Objective-C.

Setup

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

In Android Studio / IntelliJ, Flutter plugin has 2 menus to ease opening the Android Module in Android Studio & iOS Module in Xcode.

Platform Channel

Platform Channel is a channel to communicate asynchronously with native. By sending a message & receiving response asynchronously, it is guaranteed that it won’t block the main thread and the UI will remain responsive. Flutter supports not only String data type for the message but also List, Map, int, bool & double. The serialization & deserialization for these types will be done automatically when you send or receive a message. Platform Channel consist of 3 part: MethodChannel class on dart / Flutter, MethodChannel class on Android and FlutterMethodChannel class on iOS.

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

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

We need to import ‘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, this name must be the same when defined in Android & iOS. You can name it with anything, but the best practice is to name it with domain_name/channel_name. To send a message to native we use invokeMethod.

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

Because invokeMethod is an async method so we must add await and wrap this call inside an async (Future) method. If there’s 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, to be sent along with a message.

Next, we’ll start deep diving into native communication with Android & 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 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()
}

If you 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 didn’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 created.
  2. Always (cold) restart your app (pressing stop then run) if you made any change in native part because code changes in native can’t be hot reloaded or hot restarted.

Setup iOS module with Cocoapod

Before we start diving in iOS module, when creating a project,  Flutter also setup Cocoapods inside our module. To use it, inside ios folder, open podfile and pods before end block inside target ‘Runner’. Do not change anything above it as they are 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 send 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 & iOS Module even though you’ve added use_frameworks! inside podfile.