fluttermacosinfo.plistflutter-desktop

Flutter Desktop MacOS: how to open a file from Finder with Flutter app


I've written a Flutter Desktop MacOS application that uses a command line argument to process a file:

void main(List<String> args) async {
  if (args.isNotEmpty) {
    runApp(MyApp(args.first))
  } else ...;
}

which works as expected when I run it from the shell:

# this command is ok:
/Applications/TommyView.app/Contents/MacOS/TommyView Pictures/hey.png

But when I assign this app to all *.png images, and want to run it from Finder, it shows:

enter image description here

(or sometimes another error depending on Info.plist: TommyView cannot open files in the “PNG image” format.)

Also I noticed that the execution goes to "else" case (i.e. args are empty).

I guess some magic is missing in Info.plist. Please help to figure out.


Solution

  • Thanks, @smorgan, for your response. Let me improve your answer by adding a piece of code for other Flutter developers:

    1. In MainFlutterWindow.swift add the following:
    class MainFlutterWindow: NSWindow {
        open var currentFile: String? // add this variable
    
      override func awakeFromNib() {
        ...
    
        // interop with Flutter
        let channel = FlutterMethodChannel(name: "myChannel", binaryMessenger: flutterViewController.engine.binaryMessenger)
        channel.setMethodCallHandler({
            (call: FlutterMethodCall, result: FlutterResult) -> Void in
            if (call.method == "getCurrentFile") {
                result(self.currentFile)
            } else {
                result(FlutterMethodNotImplemented)
            }
        })
        ...
      }
    }
    
    1. in AppDelegate.swift you need to handle openFile:
    @NSApplicationMain
    class AppDelegate: FlutterAppDelegate {
        ...
        // called when a user double-clicks on a file in Finder
        //override func application(_ sender: NSApplication, openFile filename: String) -> Bool {
            //(mainFlutterWindow as! MainFlutterWindow).currentFile = filename
          //return true
        //}
    
        // update 2024:
        override func application(_ application: NSApplication, open urls: [URL]) {
          if (!urls.isEmpty) {
            (mainFlutterWindow as! MainFlutterWindow).currentFile = urls.first!.path
          }
        }
    }
    
    1. now in your main.dart:
    void main(List<String> args) async {
      WidgetsFlutterBinding.ensureInitialized();
      final startFile = await getStartFile(args);
      runApp(MyApp(startFile));
    }
    
    Future<String> getStartFile(List<String> args) async {
      if (args.isNotEmpty) return args.first;
      if (Platform.isMacOS) {
        // in MacOS, we need to make a call to Swift native code to check if a file has been opened with our App
        const hostApi = MethodChannel("myChannel");
        final String? currentFile = await hostApi.invokeMethod("getCurrentFile");
        if (currentFile != null) return currentFile;
      }
      return "";
    }
    
    1. [Optional] add file extensions to Info.plist to make it "visible" in MacOS recommended apps:
    <dict>
        ...
        <key>CFBundleDocumentTypes</key>
        <array>
            <dict>
                <key>CFBundleTypeExtensions</key>
                <array>
                    <string>jpg</string>
                    <string>jpeg</string>
                    <string>png</string>
                    <string>gif</string>
                    <string>webp</string>
                    <string>bmp</string>
                    <string>wbmp</string>
                </array>
                <key>CFBundleTypeRole</key>
                <string>Viewer</string>
            </dict>
        </array>
    

    I hope it will help.