persistent_window_manager
Saves and restores a Flutter desktop window's position, size, maximized, and full-screen state between sessions.
Built on top of window_manager, screen_retriever, and hydrated_bloc.
Features
- Zero-flicker restoration: Restores the last saved size, position, and window state before displaying the window on startup — once the one-time native setup below is applied (without it, the OS/engine may briefly show the window with default geometry first; see Native runner setup).
- Smart polling: Samples window geometry every 750 ms and persists changes only when the geometry actually differs — no UI thread blocking during live resizing.
- Off-screen prevention: Ensures windows aren't restored outside visible bounds if monitor setups change.
- Cross-platform safety: Automatically no-ops on web and mobile platforms, keeping your
main()unified across all targets.
Supported Platforms
- Windows
- macOS
- Linux
Setup
1. Add dependency
Add the package to your pubspec.yaml:
dependencies:
persistent_window_manager: ^3.1.0
2. Initialize HydratedBloc.storage
HydratedBloc.storage must be initialized before the package is used. Keeping this explicit ensures your app retains full control over storage directory logic:
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: kIsWeb
? HydratedStorageDirectory.web
: HydratedStorageDirectory((await getApplicationSupportDirectory()).path),
);
// ...
}
3. Replace runApp
Replace runApp with runAppPersistentWindowManager — it performs window setup before Flutter renders its first frame, then calls runApp for you:
import 'package:flutter/material.dart';
import 'package:persistent_window_manager/persistent_window_manager.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// ... HydratedBloc.storage initialization ...
await runAppPersistentWindowManager(
const MyApp(),
windowOptions: const CustomWindowOptions(
minimumSize: Size(700, 600),
title: 'My App',
),
);
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: const Center(child: Text('Hello World')),
),
);
}
}
Note: runAppPersistentWindowManager automatically handles platform detection. On web and mobile platforms, it simply calls runApp without any window management. This allows you to write a unified main() function that works across all targets.
See example/lib/main.dart for a complete runnable implementation.
4. Native runner setup
This step is what makes restoration truly flicker-free. window_manager (and therefore this package) only controls window visibility through its own show()/hide() calls — it has no say over the fact that the default Flutter desktop runner shows the window on its own as soon as it's ready, independently of and often before your saved position/size/maximized state has been applied. Without this step you may briefly see the window appear at a default position and then jump to its restored geometry.
Run once, from the root of your app:
dart run persistent_window_manager:setup
Add --dry-run to preview the changes without writing anything:
dart run persistent_window_manager:setup --dry-run
This patches, when present:
windows/runner/win32_window.cpp— removesWS_VISIBLEfrom window creation, if present.windows/runner/flutter_window.cpp— disables the automaticthis->Show()call insideSetNextFrameCallback.linux/my_application.cc— disables/replaces the automatic show (handles both the legacy and the current GTK template).macos/Runner/MainFlutterWindow.swift— adds anorder(_:relativeTo:)override callingwindow_manager'shiddenWindowAtLaunch(). The command is safe to re-run: every change is idempotent, and any file whose content doesn't exactly match a known template is left untouched and reported instead, so you can apply it by hand. Reviewgit diffafterwards before committing, as with any generated change to native project files.
Manual setup (if the script reports a file as unrecognized)
windows/runner/win32_window.cpp — in the CreateWindow call, remove | WS_VISIBLE:
- window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE,
+ window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
windows/runner/flutter_window.cpp — remove the this->Show(); call inside SetNextFrameCallback (leave ForceRedraw(), if present, untouched):
flutter_controller_->engine()->SetNextFrameCallback([&]() {
- this->Show();
});
linux/my_application.cc — depending on your Flutter SDK version, you'll have one of two variants. Modern templates connect the show to the view's first-frame signal:
static void first_frame_cb(MyApplication* self, FlView* view) {
- gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
}
Older templates show the window immediately after creation:
gtk_window_set_default_size(window, width, height);
- gtk_widget_show(GTK_WIDGET(window));
+ gtk_widget_realize(GTK_WIDGET(window));
macos/Runner/MainFlutterWindow.swift — add the import and override:
import Cocoa
import FlutterMacOS
+ import window_manager
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
// ...
super.awakeFromNib()
}
+ override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
+ super.order(place, relativeTo: otherWin)
+ hiddenWindowAtLaunch()
+ }
}
Support & Contributions
If this package saved you time or made your Flutter desktop development smoother, consider supporting its development!
(And let's be honest, tea is far superior to coffee anyway 🫖)