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:iolibrary 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.