How do I use a Flutter MethodChannel to invoke a method in dart code from the native swift code?

1,898

Solution 1

Issue

The issue is your platform side (iOS in this case) is calling a method on the Flutter side before Flutter is ready. There is no way to check from the platform side, so your Flutter app must tell your platform side. You'll have the same problem on Android.

Solution

To overcome this, you have to tell the Platform side that the app is ready (by sending a platform method) and save it in a boolean, or instantiate a class, and calling a method. Then the platform side can start sending messages.

You should really read the logs, it should warn you something along the lines of: "There is nothing listening to this, or the Flutter Engine is not attached".

import 'dart:async';

import 'package:flutter/src/services/platform_channel.dart';

class StringService {
  final methodChannel =
      const MethodChannel("com.example.app_name.method_channel.strings");

  final StreamController<String> _stringStreamController =
      StreamController<String>();

  Stream<String> get stringStream => _stringStreamController.stream;

  StringService() {
    // Set method call handler before telling platform side we are ready to receive.
    methodChannel.setMethodCallHandler((call) async {
      print('Just received ${call.method} from platform');
      if (call.method == "new_string") {
        _stringStreamController.add(call.arguments as String);
      } else {
        print("Method not implemented: ${call.method}");
      }
    });
    // Tell platform side we are ready!
    methodChannel.invokeMethod("isReady");
  }
}

You can see a working project at reverse_platform_methods, especially AppDelegate.swift. I didn't implement it for Android, but you can do it in a similar way in MainActivity.kt.

Screenshot of iOS app running the project

Question

Most apps don't want code to call from the platform side first. What is your use case? I can possibly provide better advice depending on your answer. I implemented this to handle push notifications being delivered to the device, so the "event" is definitely triggered from the platform side.

Also, you should show errors and warnings if you face them, e.g. No implementation found for method $method on channel $name'.

Solution 2

Well, the problem is all about the initialization process. You try to call your method from swift code BEFORE the dart/flutter part is ready to handle it.

You have to do the next steps to achieve the result:

  1. Important. Use applicationDidBecomeActive method in your AppDelegate for ios
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    
 var methodChannel: FlutterMethodChannel? = nil

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {

    print("Setup methodChannel from Swift")
    let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
    methodChannel = FlutterMethodChannel(name: "com.example.channeltest/changetext", binaryMessenger: rootViewController as! FlutterBinaryMessenger)

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    
    //THIS METHOD
    override func applicationDidBecomeActive(_ application: UIApplication) {
        methodChannel?.invokeMethod("some_method_name", arguments: "ios string")
    }
}

For Android onStart() method:

class MainActivity : FlutterActivity() {
    var channel: MethodChannel? = null

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        channel = MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            "com.example.channeltest/changetext"
        )

    }

    override fun onStart() {
        super.onStart()
        channel?.invokeMethod("some_method_name", "android str")
    }
}
  1. Create your own class with MethodChannel(like from prev. answer)
class TestChannel {
  static MethodChannel channel =
  const MethodChannel("com.example.channeltest/changetext");

  final StreamController<String> _controller =
  StreamController<String>();

  Stream<String> get stringStream => _controller.stream;

  TestChannel() {
    channel.setMethodCallHandler((call) async {
      if (call.method == "some_method_name") {
        _controller.add(call.arguments as String);
      } else {
        print("Method not implemented: ${call.method}");
      }
    });
  }
}
  1. Important. Create it global instance
final _changeTextChannel = TestChannel(); //<--- like this

void main() {
  runApp(MyApp());
}
  1. Handle it in UI
class TestPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: StreamBuilder<String>(
        stream: _changeTextChannel.stringStream,
        builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
          if (snapshot.hasError) {
            return Text("Error");
          }

          if (!snapshot.hasData) {
            return Text("Loading");
          }

          return Text(snapshot.data ?? "NO_DATA");
        },
      )),
    );
  }
}
Share:
1,898
Scott Clements
Author by

Scott Clements

Updated on December 25, 2022

Comments

  • Scott Clements
    Scott Clements over 1 year

    I have looked at many similar questions on this topic, but none of the solutions have worked for me.. I am developing an App in Flutter, but want to call a specific method in my main.dart file from AppDelegate.swift in the native iOS project.

    To remove all other variables I have extracted the issue into a fresh dart project. I am trying to call setChannelText() from AppDelegate.swift using methodChannel.invokeMethod(), but with no success.

    Does anybody know where I am going wrong? I know I'm not acting upon the "name" parameter in methodChannel.invokeMethod(), but that's because I only want the call to invoke the method at all...

    Here is my main.dart file:

    import 'package:flutter/material.dart';
    import 'package:flutter/services.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class MyApp extends StatefulWidget {
      @override
      _MyAppState createState() => _MyAppState();
    }
    
    class _MyAppState extends State<MyApp> {
      MethodChannel channel =
          new MethodChannel("com.example.channeltest/changetext");
      String centerText;
      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          home: Scaffold(
            backgroundColor: Colors.purple,
            body: Center(
              child: Text(
                centerText,
                style: TextStyle(
                  color: Colors.white,
                  fontWeight: FontWeight.bold,
                  fontSize: 30.0,
                ),
              ),
            ),
          ),
        );
      }
    
      @override
      void initState() {
        super.initState();
        this.channel.setMethodCallHandler((call) async => await setChannelText());
        this.centerText = "Hello World!";
      }
    
      Future setChannelText() async {
        Future.delayed(Duration(milliseconds: 200));
        setState(() => this.centerText = "Another Text.");
      }
    }

    And here is my AppDelegate.swift file:

    import UIKit
    import Flutter
    
    @UIApplicationMain
    @objc class AppDelegate: FlutterAppDelegate {
        var methodChannel: FlutterMethodChannel!
      override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:                 
    
    [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
        
        let rootViewController : FlutterViewController = window?.rootViewController as! FlutterViewController
        methodChannel = FlutterMethodChannel(name: "com.example.channeltest/changetext", binaryMessenger: rootViewController as! FlutterBinaryMessenger)
        
        //This call would obviously be somewhere else in a real world example, but I'm just
        //testing if I can invoke the method in my dart code at all..
        methodChannel.invokeMethod("some_method_name", arguments: nil)
        
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
      }
    }

    In the end, I am trying to get the text to change right after launch, but it doesn't.

    Screenshot of app running on iOS simulator

    Thanks in advance for any help!!