Home > front end >  Flutter Desktop MacOS: how to open a file from Finder with Flutter app
Flutter Desktop MacOS: how to open a file from Finder with Flutter app

Time:11-03

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.

CodePudding user response:

You need to declare file types that your application handles using CFBundleDocumentTypes in your Info.plist.

More importantly though, command line arguments aren't how your application will receive files to open on macOS, so that Dart code won't work. To handle files you would need to implement application(_:openFile:) (which can be called at any time, not just at launch) in your macOS Runner, and then pass the files to Dart using platform channels.

CodePudding user response:

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
    }
}
  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.

  • Related