In the initial stage, Flutter started it's journey as a mobile framework but now it has grown into something more powerful and bigger. Now, Flutter can be seamlessly used to build fully native desktop apps for Windows, macOS, and Linux from a single codebase system using Dart programming language.
Whether you are working as a sole developer developing a productivity tool or working with any Flutter app development company delivering cross-platform software to clients, Flutter desktop gives you the same hot reload speed, the same widget system, and the same rendering engine you already know from mobile, just in a resizable native window that users can run on their computers.
This tutorial walks you through everything from scratch. You will learn how to set up your environment, create your first desktop app, understand the folder structure, build a real working project with code examples, handle desktop-specific UI patterns, and package your app for release. Move forward to learn each and everything.
What Is Flutter Desktop and Why Should You Care?
Flutter desktop no longer remains as an experimental side feature. It has now reached a stage for stable status for Windows in February 2022, followed by macOS and Linux, and in 2026 it has become completely production-ready target that thousands of developers use to ship real software.
Flutter Desktop vs. Electron: What's the Difference?
Electron bundles a full Chromium browser inside your app to render the UI, which is why Electron apps are often large and slow to launch. Flutter desktop compiles your Dart code directly to native machine code no browser, no JavaScript runtime, no extra overhead attached.
Flutter Draws Its Own UI: It Doesn't Use Native Widgets
Unlike React Native or other frameworks that map components to native OS controls, Flutter renders every pixel itself using the Impeller graphics engine. This means your app looks exactly the same on Windows, macOS, and Linux no platform-specific styling bugs to hunt down.
A Single Codebase for Three Operating Systems
You write your app once in Dart. Flutter builds it as a .exe for Windows, a .app for macOS, and an ELF binary for Linux. The same lib/ folder, the same widgets, the same business logic just different build targets.
Desktop Is Stable and Actively Maintained
Flutter desktop is not a beta feature. As of Flutter 3.19 (released in 2024), all three desktop platforms are fully supported with regular performance improvements, bug fixes, and growing package ecosystem support from the community.
Setting Up Your Flutter Desktop Environment
Before you write a single line of code, your environment needs to be ready. Here's exactly what to install on each platform no guesswork.
Install the Flutter SDK (Version 3.19 or Later)
Download the Flutter SDK from flutter.dev and extract it to a path without spaces or special characters. Add flutter/bin to your system's PATH variable so the flutter command works in any terminal. On Windows, do this through System Properties → Environment Variables → Path.
Run flutter doctor to Check Everything
After installation, open a terminal and run:
flutter doctor
This command checks for missing tools, misconfigured SDKs, and platform-specific requirements. Fix every issue it reports before moving forward; ignoring warnings here causes build failures later.
Windows: Install Visual Studio With C++ Workload
Flutter desktop on Windows needs Visual Studio 2022 (not VS Code) with the Desktop development with C++ workload installed. This provides the MSVC compiler Flutter uses to build Windows binaries. Without it, flutter run -d windows will fail immediately.
macOS: Install Xcode and CocoaPods
Install Xcode from the Mac App Store, then run this in your terminal:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
sudo gem install cocoapods
CocoaPods is required because Flutter's macOS plugin system uses it for dependency management. Skip it and plugin builds will break.
Linux: Install GTK Development Libraries
On Ubuntu or any Debian-based Linux, run:
sudo apt-get install clang cmake ninja-build pkg-config libgtk-3-dev
Flutter uses GTK3 as the windowing backend on Linux. These packages give Flutter the low-level libraries it needs to create and manage native windows on your desktop.
Enable Your Desktop Target
Run the appropriate command for your platform:
flutter config --enable-windows-desktop
flutter config --enable-macos-desktop
flutter config --enable-linux-desktop
Without this step, your platform won't appear as a device option when you run flutter devices. This is the most commonly skipped step that confuses beginners.
Creating Your First Flutter Desktop App
With the environment ready, let's create a real project and see it run in a native desktop window.
Create the Project
flutter create my_desktop_app
cd my_desktop_app
This scaffolds a complete Flutter project with platform-specific folders alongside your lib/ directory. You'll see windows/, macos/, and linux/ folders each containing native build configuration files Flutter uses to compile for that OS.
Run It on Your Desktop
flutter run -d windows # or -d macos / -d linux
A native window opens with the default Flutter counter app. It's resizable, it has a title bar, and it responds to your OS's window controls minimize, maximize, close just like any real desktop application.
Hot Reload Works Here Too
While the app is running, press r in the terminal to trigger hot reload. Changes to your Dart code in lib/ reflect instantly in the window. This is the same hot reload experience from mobile development it works identically on desktop and saves enormous amounts of time during UI iteration.
The Folder Structure You Need to Understand
Your lib/main.dart is where your Dart code lives. The windows/, macos/, and linux/ folders contain the native project files a Visual Studio solution for Windows, an Xcode workspace for macOS, and CMake files for Linux. You rarely need to touch these manually, but knowing they exist helps when platform-specific build errors appear.
Building a Real App: A Simple Note-Taking Tool
Rather than just explaining the counter app, let's build something you'd actually use a note-taking app where you can type notes and save them to a file. This covers real desktop patterns: multi-line text input, file system access, and a clean layout.
The Complete main.dart
Replace the contents of lib/main.dart with this:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'NoteDesk',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
useMaterial3: true,
),
home: const NoteScreen(),
);
}
}
class NoteScreen extends StatefulWidget {
const NoteScreen({super.key});
@override
State<NoteScreen> createState() => _NoteScreenState();
}
class _NoteScreenState extends State<NoteScreen> {
final TextEditingController _controller = TextEditingController();
String _status = 'Start typing your note...';
Future<void> _saveNote() async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/my_note.txt');
await file.writeAsString(_controller.text);
setState(() {
_status = 'Note saved to ${file.path}';
});
}
Future<void> _loadNote() async {
final directory = await getApplicationDocumentsDirectory();
final file = File('${directory.path}/my_note.txt');
if (await file.exists()) {
final content = await file.readAsString();
setState(() {
_controller.text = content;
_status = 'Note loaded successfully';
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('NoteDesk'),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.folder_open),
tooltip: 'Load Note',
onPressed: _loadNote,
),
IconButton(
icon: const Icon(Icons.save),
tooltip: 'Save Note',
onPressed: _saveNote,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Expanded(
child: TextField(
controller: _controller,
maxLines: null,
expands: true,
keyboardType: TextInputType.multiline,
style: const TextStyle(fontSize: 16),
decoration: const InputDecoration(
hintText: 'Write your note here...',
border: OutlineInputBorder(),
),
),
),
const SizedBox(height: 12),
Text(
_status,
style: const TextStyle(color: Colors.grey, fontSize: 13),
),
],
),
),
);
}
}
Add the path_provider Package
Open pubspec.yaml and add this under dependencies:
dependencies:
flutter:
sdk: flutter
path_provider: ^2.1.2
Then run:
flutter pub get
The path_provider package gives you access to the user's Documents folder on all three desktop platforms Windows, macOS, and Linux with a single API call. No platform-specific code needed.
What This Code Does
The _saveNote() function gets the app's documents directory using getApplicationDocumentsDirectory(), then writes the text from the TextField into a .txt file using Dart's built-in File class. The _loadNote() function reads that same file back and fills the text field. Both functions update a status message at the bottom so the user always knows what just happened.
Run It and Test
flutter run -d windows
Type something in the text field, click the save icon, and check your Documents folder the file is there. Click the load icon to pull it back. This is full file system access in a Flutter desktop app, working out of the box.
Desktop-Specific UI Patterns You Need to Know
Mobile UI doesn't automatically feel right on desktop. There are patterns desktop users expect that you need to add intentionally.
Setting Window Title and Minimum Size
Add the window_manager package to pubspec.yaml:
window_manager: ^0.3.8
Then update your main() function:
import 'package:window_manager/window_manager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
size: Size(900, 600),
minimumSize: Size(600, 400),
title: 'NoteDesk',
center: true,
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
runApp(const MyApp());
}
This sets the window to 900×600 pixels on launch, prevents the user from resizing below 600×400 (which would break your layout), centers it on screen, and sets the title bar text. These are basic details every desktop app should handle and most Flutter tutorials skip them entirely.
Adding Keyboard Shortcuts
Desktop users rely on keyboard shortcuts. Add Ctrl+S to save the note by wrapping your widget tree:
@override
Widget build(BuildContext context) {
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS):
const SaveIntent(),
},
child: Actions(
actions: {
SaveIntent: CallbackAction<SaveIntent>(
onInvoke: (intent) => _saveNote(),
),
},
child: Focus(
autofocus: true,
child: Scaffold(
// ... your existing scaffold
),
),
),
);
}
class SaveIntent extends Intent {
const SaveIntent();
}
This maps Ctrl+S directly to the _saveNote() function. On macOS, use LogicalKeyboardKey.meta instead of LogicalKeyboardKey.control for the Command key. Keyboard shortcuts like this make your app feel native users reach for them instinctively on desktop.
Cursor Changes on Hover
Wrap any clickable element with MouseRegion to show the correct cursor:
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: _saveNote,
child: const Text('Save'),
),
)
This is a small thing that makes a noticeable difference. When users hover over a button and the cursor doesn't change to a pointer, the app feels unfinished. On mobile this doesn't matter on desktop it absolutely does.
Packaging and Distributing Your Flutter Desktop App
Building locally is one thing. Getting your app onto other people's computers requires proper packaging for each platform.
Build a Release Binary for Windows
flutter build windows --release
The output lives in build/windows/x64/runner/Release/. This folder contains your .exe file and several required .dll files. You must distribute the entire folder the .exe alone won't run on another machine without its DLL dependencies sitting next to it.
Create a Windows Installer With the msix Package
Add msix to your dev dependencies:
dev_dependencies:
msix: ^3.16.7
Add this block to your pubspec.yaml:
msix_config:
display_name: NoteDesk
publisher_display_name: Your Name
identity_name: com.yourname.notedesk
msix_version: 1.0.0.0
logo_path: assets/icon.png
Then run,
flutter pub run msix:create
This generates a .msix installer file that users can double-click to install your app cleanly on Windows 10 and 11. It handles file associations, start menu entries, and uninstallation automatically.
Build for macOS
flutter build macos --release
The .app bundle is in build/macos/Build/Products/Release/. For distributing outside the Mac App Store, you need to codesign with an Apple Developer certificate and run Apple's notarization process. Unsigned apps trigger Gatekeeper warnings users see a scary "this app can't be opened" message, which kills adoption fast.
Build for Linux and Create an AppImage
flutter build linux --release
The output is in build/linux/x64/release/bundle/. Package it as an AppImage using appimagetool so it runs on any Linux distribution without requiring installation. AppImages are self-contained and portable they're the most practical Linux distribution format for Flutter desktop apps.
Common Mistakes to Avoid
Every developer runs into these when building their first Flutter desktop app. Knowing them in advance saves you hours of debugging.
Using Packages Without Checking Desktop Support
Not every pub.dev package works on desktop. Always check the platform support badges on the package page before adding it to your project. Packages that use Android or iOS platform channels will throw MissingPluginException at runtime on desktop and that error won't appear until you actually run the relevant code path.
Forgetting to Handle the Window Close Event
If your app has unsaved data, intercept the close event before the window disappears. Use windowManager.setPreventClose(true) and implement the WindowListener interface to show a confirmation dialog. Without this, users lose their work silently which is unacceptable in a desktop app.
Hard-Coding Mobile-Scale Touch Targets
Buttons designed for 48dp mobile touch targets feel oversized on desktop where users have a mouse cursor that's pixel-precise. Scale down your padding and interactive element sizes for desktop layouts. Users notice when everything feels puffed up and designed for thumbs instead of a pointer.
Ignoring Window Size and State Persistence
Desktop users expect apps to remember their window size and position between sessions. Save the window dimensions on close and restore them on launch using window_manager. An app that always opens at the same fixed size in the center of the screen feels rigid and unpolished compared to every other app on the desktop.
Final Words
Flutter desktop has genuinely arrived. The tooling is solid, the performance is native, and the developer experience is exactly what you'd expect from a Flutter app development company shipping production software. You get hot reload, a massive widget library, full file system access, and a single codebase that targets Windows, macOS, and Linux without compromise.
The note-taking app in this tutorial is simple by design but the patterns it demonstrates scale directly to complex real-world software. File I/O, keyboard shortcuts, window management, and proper packaging all follow the same principles whether you're building a text editor or a full enterprise desktop tool.
Start with something small, get it running on your machine, then build from there. Flutter desktop rewards developers who take it seriously and the bar for standing out is still low enough that a well-built app gets noticed.
Top comments (0)