In our previous installment, we saw how to specify which Flutter page we want to navigate to from the native side of our app. Next, we are going to give the user a way to dismiss the presented FlutterViewController or FlutterActivity/FlutterFragment. In order to do this, we need to let the Flutter and native sides of our app communicate with one another. Facilitating this communication between Flutter and native is the role of method channels.

Before moving forward, be sure to follow along with the example project. If you are continuing from the previous article, no further action is required. If you haven't yet worked with the example project, be sure to clone it and check out the part-iv branch. After cloning, cd into FlutterIntegration_Flutter and run flutter pub get. Then cd into FlutterIntegration_iOS and run pod install. You should now be ready to work with the example project.

With method channels, we can set up a channel between the Flutter and native sides of our project. We can then serialize data into a dictionary and send that data to the other side. Let's take a look at how this is done.

Method channels facilitate communication between integrated Flutter code and native code.

Configuring Flutter to invoke method channel calls

Configuring the Flutter project to invoke a method channel call is easy. We simply create a method channel and call invokeMethod on said method channel. As mentioned, you can serialize and pass data across this channel, but we are simply going to invoke the method and listen for this invocation on the native sides. 

Let's head over to main.dart and wrap our Text widget in HelloWorldPage in a Column and add a FlatButton under the Text widget: 

class HelloWorldPage extends StatelessWidget {
  final bool isInGerman;

  HelloWorldPage({this.isInGerman = false});

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(isInGerman ? 'Hallo Welt!' : 'Hello World!'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Text(
                isInGerman
                    ? 'Dies ist eine vollständig Flutter Seite, die in Ihre native App integriert ist!'
                    : 'This is a fully Flutter page, integrated into your native app!',
                textAlign: TextAlign.center,
              ),
            ),
            FlatButton(
              color: Colors.blue,
              onPressed: () {},
              child: Text(
                isInGerman ? 'Geh Zurück' : 'Go Back',
                style: Theme.of(context)
                    .textTheme
                    .bodyText1
                    .copyWith(color: Colors.white),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Now, we simply add MethodChannel('HelloWorldMethodChannel').invokeMethod('dismiss'); to our FlatButton's onPressed handler. In order to use MethodChannel, we also have to put import 'package:flutter/services.dart'; at the top of our main.dart file, just under import 'package:flutter/material.dart';.

FlatButton(
    color: Colors.blue,
    onPressed: () {
        MethodChannel('HelloWorldMethodChannel')
            .invokeMethod('dismiss');
    },
    child: Text(
        isInGerman ? 'Geh Zurück' : 'Go Back',
        style: Theme.of(context)
            .textTheme
            .bodyText1
            .copyWith(color: Colors.white),
    ),
),

Listening for method channel invocation on iOS

To listen for calls on the "HelloWorld" Method Channel, we only have to add a small chunk of code to our iOS project. This code creates a method channel with the "HelloWorldMethodChannel" name and with our engine's binaryMessenger. It then sets a handler that listens for method channel calls, checks the name of the method, and runs code depending on what method was called. In our case, we are wanting to dismiss the HelloWorldFlutterViewController whenever the "dismiss" method is called:

class HelloWorldFlutterViewController: FlutterViewController {
    
    init(engine: FlutterEngine) {
        super.init(engine: engine, nibName: nil, bundle: nil)
        
        FlutterMethodChannel(name: "HelloWorldMethodChannel", binaryMessenger: engine.binaryMessenger).setMethodCallHandler { (call, result) in
            if call.method == "dismiss" {
                self.dismiss(animated: true, completion: nil)
            }
        }
    }
    
    ...

}

Listening for method channel invocation on Android

Much like listening to method channel invocation on iOS, we only need a small chunk of code to listen within our Android project. This code is doing basically the same thing as well, i.e. creating a method channel with the "HelloWorldMethodChannel" name and with our engine's binaryMessenger and creating a handler that listens for and reacts to method channel calls. To do this, we have to override the configureFlutterEngine(FlutterEngine) method on our HelloWorldFlutterActivity:

package com.wwt.flutterintegrationexample

import android.content.Context
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineCache
import io.flutter.plugin.common.MethodChannel

class HelloWorldFlutterActivity : FlutterActivity() {
    
    ...

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "HelloWorldMethodChannel").setMethodCallHandler { call, _ ->
            if (call.method == "dismiss") {
                this.finish()
            }
        }
    }
    
    ...
    
}

Wrapping up

Whew, that was a lot! But hopefully, this sample project and tutorial will help you and your team more easily begin to migrate your native app to a Flutter app. This overview has covered the basics of integrating Flutter into an existing native app, but depending on your needs, there will likely be some unexpected "gotchas" that you'll run into. We plan on covering some of what we ran into and how we solved these problems in a future installment.

To see what we've done with Flutter integration, download ATC Connect by clicking one of the links below and signing in by requesting a magic link using your work or personal email address.

I hope you found the information in this article valuable. If you are in the process of migrating a native app to Flutter or have questions about the process, feel free to leave a comment below and start a conversation with us!