Handling Desktop-Specific Features in Flutter

Flutter, Google’s UI toolkit, is known for its cross-platform capabilities, allowing developers to write code once and deploy it across multiple platforms, including mobile (Android and iOS), web, and desktop (Windows, macOS, and Linux). However, sometimes you need to implement features that are specific to a desktop environment, such as menu bars, native dialogs, or integration with the operating system. In this comprehensive guide, we’ll explore how to handle desktop-specific features in Flutter.

Understanding Platform-Specific Code in Flutter

Flutter provides several ways to write platform-specific code:

  • Conditional Compilation: Using Dart’s dart:io library with conditional imports to include platform-specific code.
  • Platform Channels: Communicating with native platform code written in Swift/Objective-C (for macOS) or C++ (for Windows/Linux).
  • Platform Widgets: Using widgets that adapt to the look and feel of the target platform.

Method 1: Conditional Compilation

Conditional compilation allows you to include different code based on the target platform. This is useful for implementing small platform-specific features directly in Dart.

Step 1: Import the dart:io Library Conditionally

import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;

String getOperatingSystem() {
  if (kIsWeb) {
    return 'Web';
  } else if (Platform.isAndroid) {
    return 'Android';
  } else if (Platform.isIOS) {
    return 'iOS';
  } else if (Platform.isMacOS) {
    return 'macOS';
  } else if (Platform.isWindows) {
    return 'Windows';
  } else if (Platform.isLinux) {
    return 'Linux';
  } else {
    return 'Unknown';
  }
}

Step 2: Use Conditional Checks in Your Code

import 'package:flutter/material.dart';
import 'dart:io' show Platform;
import 'package:flutter/foundation.dart' show kIsWeb;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Desktop Specific Features',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Desktop Specific Features'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('Running on: ${getOperatingSystem()}'),
            if (Platform.isMacOS)
              Text('This is macOS specific feature!'),
            if (Platform.isWindows)
              Text('This is Windows specific feature!'),
            if (Platform.isLinux)
              Text('This is Linux specific feature!'),
          ],
        ),
      ),
    );
  }

  String getOperatingSystem() {
    if (kIsWeb) {
      return 'Web';
    } else if (Platform.isAndroid) {
      return 'Android';
    } else if (Platform.isIOS) {
      return 'iOS';
    } else if (Platform.isMacOS) {
      return 'macOS';
    } else if (Platform.isWindows) {
      return 'Windows';
    } else if (Platform.isLinux) {
      return 'Linux';
    } else {
      return 'Unknown';
    }
  }
}

Method 2: Platform Channels

For more complex interactions with the native platform, you can use Platform Channels. This involves writing native code in Swift/Objective-C (macOS) or C++ (Windows/Linux) and communicating with it from Flutter.

Step 1: Set Up the Platform Channel

In your Flutter code, define a MethodChannel:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Platform Channels Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('desktop_features');
  String _message = 'Press the button';

  Future<void> _getMessageFromNative() async {
    String message;
    try {
      final String result = await platform.invokeMethod('getMessage');
      message = 'Native message: $result';
    } on PlatformException catch (e) {
      message = "Failed to get message: '${e.message}'.";
    }

    setState(() {
      _message = message;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Platform Channels Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(_message),
            ElevatedButton(
              onPressed: _getMessageFromNative,
              child: Text('Get Native Message'),
            ),
          ],
        ),
      ),
    );
  }
}

Step 2: Implement Native Code

For macOS (Swift):

import FlutterMacOS
import Cocoa

@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}

class MainFlutterWindow: NSWindow {
  override func awakeFromNib() {
    let flutterViewController = FlutterViewController()
    let windowFrame = self.frame
    self.contentViewController = flutterViewController
    self.setFrame(windowFrame, display: true)

    RegisterGeneratedPlugins(registry: flutterViewController)

    // Set up method channel
    let methodChannel = FlutterMethodChannel(name: "desktop_features", binaryMessenger: flutterViewController.engine.binaryMessenger)
    methodChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
      if call.method == "getMessage" {
        result("Hello from macOS!")
      } else {
        result(FlutterMethodNotImplemented)
      }
    }

    super.awakeFromNib()
  }
}

For Windows (C++):

#include <flutter_windows.h>
#include <iostream>
#include <Windows.h>

namespace {

  class MethodChannelProvider : public flutter::Plugin {
  public:
    static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) {
      auto channel =
        std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
          registrar->messenger(), "desktop_features",
          &flutter::StandardMethodCodec::GetInstance());

      auto plugin = std::make_unique<MethodChannelProvider>();

      channel->SetMethodCallHandler(
        [plugin_pointer = plugin.get()](
          const auto& call, auto result) {
            plugin_pointer->HandleMethodCall(call, std::move(result));
        });

      registrar->AddPlugin(std::move(plugin));
    }

  private:
    MethodChannelProvider() {}

    void HandleMethodCall(
      const flutter::MethodCall& call,
      std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
      if (call.method_name().compare("getMessage") == 0) {
        result->Success(flutter::EncodableValue("Hello from Windows!"));
      } else {
        result->NotImplemented();
      }
    }
  };

}  // namespace

void RegisterPlugin(flutter::PluginRegistrar* registrar) {
  MethodChannelProvider::RegisterWithRegistrar(registrar);
}

Step 3: Register the Plugin

Register the plugin in the appropriate platform-specific code. On macOS, the generated plugin registrar will typically handle this registration automatically in `GeneratedPluginRegistrant.swift`. For Windows, modify the `FlutterWindow::OnCreate` function in `flutter_window.cpp`:

#include "flutter_window.h"
#include <flutter_plugins.h>

#include <iostream>

FlutterWindow::FlutterWindow(const flutter::DartProject& dart_project)
    : dart_project_(dart_project) {}

FlutterWindow::~FlutterWindow() = default;

bool FlutterWindow::OnCreate() {
  if (!Win32Window::OnCreate()) {
    return false;
  }

  RECT frame = GetClientAreaSize();

  // Attach the FlutterView.
  flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
      frame.right - frame.left, frame.bottom - frame.top, dart_project_);
  // Ensure that basic setup of the controller was successful.
  if (!flutter_controller_->CreateWindow(frame.right - frame.left, frame.bottom - frame.top, child_handle())) {
    return false;
  }
  RegisterPlugins(flutter_controller_->engine()); // Register plugins

  SetChildContent(flutter_controller_->view()->GetNativeWindow());

  return true;
}

Method 3: Platform Widgets

Flutter’s adaptive widgets can automatically adapt to the look and feel of the target platform. While these widgets may not cover all desktop-specific UI needs, they can help create a more native-feeling experience.

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:io' show Platform;

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Adaptive Widgets Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Adaptive Widgets Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            if (Platform.isIOS)
              CupertinoButton(
                child: Text('Cupertino Button'),
                onPressed: () {},
              ),
            if (Platform.isAndroid)
              ElevatedButton(
                child: Text('Material Button'),
                onPressed: () {},
              ),
            if (Platform.isMacOS) // Using Material widgets on macOS
              ElevatedButton(
                child: Text('Material Button (macOS)'),
                onPressed: () {},
              ),
            if (Platform.isWindows)  // Using Material widgets on Windows
              ElevatedButton(
                child: Text('Material Button (Windows)'),
                onPressed: () {},
              ),
             if (Platform.isLinux)  // Using Material widgets on Linux
              ElevatedButton(
                child: Text('Material Button (Linux)'),
                onPressed: () {},
              ),
          ],
        ),
      ),
    );
  }
}

Example: Implementing a Desktop Menu Bar

A common desktop-specific feature is a menu bar. Implementing this usually requires Platform Channels because Flutter does not natively provide a menu bar widget for desktop.

Step 1: Set up the Method Channel

In Flutter:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Menu Bar Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('menu_bar');

  @override
  void initState() {
    super.initState();
    _createMenuBar();
  }

  Future<void> _createMenuBar() async {
    try {
      await platform.invokeMethod('createMenuBar');
    } on PlatformException catch (e) {
      print("Failed to create menu bar: '${e.message}'.");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Menu Bar Demo'),
      ),
      body: Center(
        child: Text('Check the Menu Bar at the top!'),
      ),
    );
  }
}

Step 2: Implement Native Code

macOS (Swift):

import FlutterMacOS
import Cocoa

@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    return true
  }
}

class MainFlutterWindow: NSWindow {
  override func awakeFromNib() {
    let flutterViewController = FlutterViewController()
    let windowFrame = self.frame
    self.contentViewController = flutterViewController
    self.setFrame(windowFrame, display: true)

    RegisterGeneratedPlugins(registry: flutterViewController)

    let methodChannel = FlutterMethodChannel(name: "menu_bar", binaryMessenger: flutterViewController.engine.binaryMessenger)
    methodChannel.setMethodCallHandler { (call: FlutterMethodCall, result: @escaping FlutterResult) in
      if call.method == "createMenuBar" {
        self.createMenuBar()
        result(nil)
      } else {
        result(FlutterMethodNotImplemented)
      }
    }

    super.awakeFromNib()
  }

  func createMenuBar() {
    let mainMenu = NSMenu()

    let appMenuItem = NSMenuItem()
    let appMenu = NSMenu()
    appMenu.addItem(NSMenuItem(title: "About MyApp", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: ""))
    appMenu.addItem(NSMenuItem.separator())
    appMenu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
    appMenuItem.submenu = appMenu
    mainMenu.addItem(appMenuItem)

    let windowMenuItem = NSMenuItem()
    let windowMenu = NSMenu(title: "Window")
    windowMenu.addItem(NSMenuItem(title: "Minimize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m"))
    windowMenu.addItem(NSMenuItem(title: "Zoom", action: #selector(NSWindow.zoom(_:)), keyEquivalent: ""))
    windowMenu.addItem(NSMenuItem.separator())
    windowMenu.addItem(NSMenuItem(title: "Bring All to Front", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: ""))
    windowMenuItem.submenu = windowMenu
    mainMenu.addItem(windowMenuItem)

    NSApplication.shared.mainMenu = mainMenu
  }
}

Windows (C++): Implementing menu bars in Windows involves more complex Windows API calls. Here’s a basic outline:

#include <flutter_windows.h>
#include <iostream>
#include <Windows.h>

namespace {

class MenuBarPlugin : public flutter::Plugin {
public:
  static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) {
    auto channel =
      std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
        registrar->messenger(), "menu_bar",
        &flutter::StandardMethodCodec::GetInstance());

    auto plugin = std::make_unique<MenuBarPlugin>(registrar->view()->GetNativeWindow());

    channel->SetMethodCallHandler(
      [plugin_pointer = plugin.get()](
        const auto& call, auto result) {
          plugin_pointer->HandleMethodCall(call, std::move(result));
      });

    registrar->AddPlugin(std::move(plugin));
  }

private:
  MenuBarPlugin(HWND hwnd) : hwnd_(hwnd) {}
  HWND hwnd_;

  void HandleMethodCall(
    const flutter::MethodCall& call,
    std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
    if (call.method_name().compare("createMenuBar") == 0) {
      CreateMenuBar();
      result->Success();
    } else {
      result->NotImplemented();
    }
  }

  void CreateMenuBar() {
    HMENU hMenuBar = CreateMenu();
    HMENU hFileMenu = CreateMenu();
    HMENU hEditMenu = CreateMenu();

    AppendMenuW(hFileMenu, MF_STRING, IDM_NEW, L"&New");
    AppendMenuW(hFileMenu, MF_SEPARATOR, 0, NULL);
    AppendMenuW(hFileMenu, MF_STRING, IDM_EXIT, L"&Exit");

    AppendMenuW(hEditMenu, MF_STRING, IDM_COPY, L"&Copy");
    AppendMenuW(hEditMenu, MF_STRING, IDM_PASTE, L"&Paste");

    AppendMenuW(hMenuBar, MF_POPUP, (UINT_PTR)hFileMenu, L"&File");
    AppendMenuW(hMenuBar, MF_POPUP, (UINT_PTR)hEditMenu, L"&Edit");

    SetMenu(hwnd_, hMenuBar);
    DrawMenuBar(hwnd_);
  }

  enum {
    IDM_NEW = 1001,
    IDM_EXIT,
    IDM_COPY,
    IDM_PASTE
  };
};

}  // namespace


void RegisterPlugin(flutter::PluginRegistrar* registrar) {
  MenuBarPlugin::RegisterWithRegistrar(registrar);
}

Remember that for the Windows implementation, you also need to handle the menu commands in your window procedure. The exact implementation may vary based on your application’s requirements.

Best Practices for Handling Desktop-Specific Features

  • Keep Platform-Specific Code Separate: Use conditional compilation or Platform Channels to isolate platform-specific code, making your codebase more maintainable.
  • Use Adaptive Widgets When Possible: Leverage Flutter’s adaptive widgets to provide a consistent look and feel across platforms.
  • Provide Fallbacks: When a desktop-specific feature is not available on other platforms, provide a reasonable fallback or disable the feature gracefully.
  • Test Thoroughly: Test your application on all target platforms to ensure that desktop-specific features work as expected and do not introduce issues on other platforms.

Conclusion

Handling desktop-specific features in Flutter involves a combination of conditional compilation, Platform Channels, and adaptive widgets. By carefully structuring your code and using these techniques, you can create a Flutter application that seamlessly integrates with desktop environments while maintaining cross-platform compatibility. Whether it’s implementing menu bars, accessing native APIs, or adapting UI elements, Flutter provides the tools necessary to deliver a first-class desktop experience. Leverage the power of Platform Channels for complex native integrations, and use conditional compilation for smaller platform-specific adjustments. This approach will ensure that your Flutter app is well-optimized and delivers a tailored user experience across all supported platforms.