惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Securelist
Simon Willison's Weblog
Simon Willison's Weblog
Security Latest
Security Latest
博客园 - 司徒正美
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
C
Check Point Blog
Jina AI
Jina AI
H
Heimdal Security Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
B
Blog
美团技术团队
W
WeLiveSecurity
G
Google Developers Blog
K
Kaspersky official blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
C
Cybersecurity and Infrastructure Security Agency CISA
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
V
Visual Studio Blog
T
Threatpost
酷 壳 – CoolShell
酷 壳 – CoolShell
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Attack and Defense Labs
Attack and Defense Labs
腾讯CDC
Hacker News - Newest:
Hacker News - Newest: "LLM"
Cyberwarzone
Cyberwarzone
Apple Machine Learning Research
Apple Machine Learning Research
P
Privacy & Cybersecurity Law Blog
H
Hacker News: Front Page
博客园 - 【当耐特】
人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
CERT Recently Published Vulnerability Notes
V
Vulnerabilities – Threatpost
AI
AI
MongoDB | Blog
MongoDB | Blog
Project Zero
Project Zero
Hacker News: Ask HN
Hacker News: Ask HN
量子位
A
Arctic Wolf
GbyAI
GbyAI
Recorded Future
Recorded Future
SecWiki News
SecWiki News
小众软件
小众软件
Martin Fowler
Martin Fowler

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Flutter Desktop Mastery 1: Window Management
Kingkor Roy · 2026-05-15 · via DEV Community

There’s no debate that Flutter’s desktop support is very limited. And the Flutter community actually did an amazing job creating an ecosystem of packages to make Flutter Desktop production ready.

Building a professional-grade desktop application with Flutter requires more than just writing Dart code. Here at Spotube, we've spent years perfecting the craft of cross-platform desktop development, tackling everything from window management to system integration to multi-platform packaging. This series of article breaks down the technical decisions and implementation patterns that make Spotube a production-ready desktop application. We’ll focus on desktop specific features like Window Management, System Tray support and Desktop Notifications


1. Window Management & Lifecycle 🪟

At Spotube, window management is one of the most critical aspects of the desktop experience. We use the window_manager package to give us fine-grained control over window behavior across platforms.

Initialization & Configuration

At Spotube, we always create a wrapper for external libraries for easier refactoring and very easy mocking for unit testing.

In lib/services/wm_tools/wm_tools.dart, we initialize the window manager with platform-aware settings. Because windowManager cannot be called in Android, iOS or Web. This will cause exceptions that might crash the app.

We use Singletons Design Pattern for the services and freezed for immutable data classes

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';

part 'wm_tools.freezed.dart';
part 'wm_tools.g.dart';

// Some helper constants. Give emphasis on the kIsWeb constant, it
// must be checked before doing anything with [Platform] object.
// Platform (or more specifically dart:io) is not available on flutter web
final kIsAndroid = kIsWeb ? false : Platform.isAndroid;
final kIsIOS = kIsWeb ? false : Platform.isIOS;
final kIsMobile = kIsAndroid || kIsIOS;

@freezed
class WindowSize with _$WindowSize {
  const factory WindowSize({
    required double height,
    required double width,
    required bool maximized,
  }) = _WindowSize;

  factory WindowSize.fromJson(Map<String, dynamic> json) =>
      _$WindowSizeFromJson(json);
}

Enter fullscreen mode Exit fullscreen mode

Above is our Freezed data class WindowSize which we use to store our Window Size Information. And we must do that to ensure when user resizes the window our app remembers to restore the screen to that size on next startup.

Below is the KVStoreService which we use to save stuff to shared_preferences aka local key-value storage. This is also a Singleton and we use a separate class so we have single source of truth for all our key-value data entry and we don’t make mess using only key strings.

abstract class KVStoreService {
  static SharedPreferences? _sharedPreferences;
  static SharedPreferences get sharedPreferences => _sharedPreferences!;

  static Future<void> initialize() async {
    _sharedPreferences = await SharedPreferences.getInstance();
  }

  static WindowSize? get windowSize {
    final raw = sharedPreferences.getString('windowSize');

    if (raw == null) {
      return null;
    }
    return WindowSize.fromJson(jsonDecode(raw));
  }

  static Future<void> setWindowSize(WindowSize value) async =>
      await sharedPreferences.setString(
        'windowSize',
        jsonEncode(
          value.toJson(),
        ),
      );
}

Enter fullscreen mode Exit fullscreen mode

Now finally, this is our actual WindowManagerTools singleton service. windowManager is also a singleton from the window_manager package because currently Flutter only has a single window support.

class WindowManagerTools with WidgetsBindingObserver {
    // Classic/idiomatic Dart way of implementing singletons
  static WindowManagerTools? _instance;
  static WindowManagerTools get instance => _instance!;
  // We make the constructor private
  WindowManagerTools._();

    // This function must be called in the main.dart's main() function.
  static Future<void> initialize() async {
    await windowManager.ensureInitialized();
    _instance = WindowManagerTools._();

    // Adding this WindowManagerTools singleton as a WidgetsBinding observer
    // to recieve life-cycle updates when window is resized or moved
    WidgetsBinding.instance.addObserver(instance);

    await windowManager.waitUntilReadyToShow(
      const WindowOptions(
        title: "\"Spotube\","
        backgroundColor: Colors.transparent,
        // minimum size must be set otherwise users can shrink the windows into a
        // single pixel. I don't think anyone wants add responsiveness down to
        // 1 pixel lmao.
        minimumSize: Size(300, 700),
        titleBarStyle: TitleBarStyle.hidden, // This hides the titlebar
        // Opens the app in Center of the Monitor/screen
        center: true,
      ),
      () async {
        // Restoring window size
        final savedSize = KVStoreService.windowSize;
        await windowManager.setResizable(true);

        if (savedSize?.maximized == true &&
            !(await windowManager.isMaximized())) {
          await windowManager.maximize();
        } else if (savedSize != null) {
          await windowManager.setSize(Size(savedSize.width, savedSize.height));
        }

        await windowManager.focus();
        await windowManager.show();
      },
    );
  }

  // To ignore same size events and avoid unnecessary resizing operations
  Size? _prevSize;

    // This is why we extended from WidgetsBindingObserver which gives us 
    // these life-cycle methods that we can listen to
  @override
  void didChangeMetrics() async {
    super.didChangeMetrics();
    // We are ignoring mobile and web platforms
    if (kIsMobile || kIsWeb) return;

    // windowManager.getSize gets the current size of the Window
    final size = await windowManager.getSize();
    final windowSameDimension =
        _prevSize?.width == size.width && _prevSize?.height == size.height;

    if (windowSameDimension || _prevSize == null) {
      _prevSize = size;
      return;
    }
    final isMaximized = await windowManager.isMaximized();
    await KVStoreService.setWindowSize(
      WindowSize(
        height: size.height,
        width: size.width,
        maximized: isMaximized,
      ),
    );
    _prevSize = size;
  }
}

Enter fullscreen mode Exit fullscreen mode

Key takeaways:

  • TitleBarStyle.hidden: We hide the native title bar because we implement a custom one in Dart (more on this later)
  • Window size persistence: We save and restore window dimensions, including maximized state
  • Minimum size constraint: Prevents UI breakdown on very small windows
  • Observer pattern: By extending WidgetsBindingObserver, we listen to window resize events

Multi-window can be implemented through desktop_multi_window package but that is not the focus of this article.

Handling Close Behavior

Here's where Spotube excels, we give users control over what happens when they click the close button. In lib/hooks/configurators/use_close_behavior.dart:

void useCloseBehavior(WidgetRef ref) {
  useWindowListener(
    onWindowClose: () async {
      final preferences = ref.read(userPreferencesProvider);
      if (preferences.closeBehavior == CloseBehavior.minimizeToTray) {
        await windowManager.hide();
        closeNotification?.show();
      } else {
        exit(0);
      }
    },
  );
}

Enter fullscreen mode Exit fullscreen mode

This allows users to either:

  1. Close completely - Exit the app immediately
  2. Minimize to system tray - Keep audio playing in the background

This is set in lib/main.dart:

if (kIsDesktop) {
  await windowManager.setPreventClose(true);
  // ... other initialization
}

Enter fullscreen mode Exit fullscreen mode

By setting setPreventClose(true), we intercept the close event before it propagates to the OS, giving us control over the behavior.

Now if you know, you might be thinking the above code looks like React as it is clearly react-hooks, right?? You got exactly right. Spotube uses flutter_hooks package in combination with riverpod’s hooks_riverpod package. This reduces insane Flutter boilerplate code and helps focus on things that matter more. The useWindowListener is also a custom hook that implement window_manager’s WindowListener interface and converts it into a simple callback based hook that can be used anywhere.
Take a look at: lib\hooks\configurators\use_window_listener.dart for understanding what we’re doing.

Platform-Specific Window Fixes

In lib/hooks/configurators/use_fix_window_stretching.dart handles a quirky Windows issue where the Window content of the initial page stretches and causes weird layout that is not something a user should see.

The bug is still not fixed to this day and is actually caused by the Flutter Window embedder in Windows that does not provide proper size information on the first frame

The GitHub issue: leanflutter/window_manager#464

To fix this, a simple fix was to resize the window by 1 pixel and that fixed the issue, a workaround.

void useFixWindowStretching() {
  useEffect(() {
    if (!kIsWindows) return;
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      await Future.delayed(const Duration(milliseconds: 100), () {
        windowManager.getSize().then((Size value) {
          windowManager.setSize(
            Size(value.width + 1, value.height + 1),
          );
        });
      });
    });
    return null;
  }, []);
}

Enter fullscreen mode Exit fullscreen mode

This must be called in all the route pages that can be an initial page. We call it in our root App instance.

2. Custom Title Bar Implementation 🎨

Since we hide the native title bar, we implement a fully custom one. This gives us complete control over appearance while maintaining platform-specific behaviors.

The TitleBar Widget

TitleBar is an AppBar replacement from Flutter Material. This widget integrates window_manager and necessary paddings, gesutres and window buttons when the platform is desktop.

final kTitlebarVisible = kIsWindows || kIsLinux;

/// We're using [HookConsumerWidget] which is a widget from hooks_riverpod package
/// that allows us to use hooks and also consume providers.
///
/// The [PreferredSizeWidget] interface as it is required by the Scaffold's appBar property,
/// which allows us to specify the preferred size of the title bar.
class TitleBar extends HookConsumerWidget implements PreferredSizeWidget {
  final bool automaticallyImplyLeading;
  final List<Widget> trailing;
  final List<Widget> leading;
  final Widget? child;
  final Widget? title;
  final Widget? header; // small widget placed on top of title
  final Widget? subtitle; // small widget placed below title
  final bool
      trailingExpanded; // expand the trailing instead of the main content
  final AlignmentGeometry alignment;
  final Color? backgroundColor;
  final Color? foregroundColor;
  final double? leadingGap;
  final double? trailingGap;
  final EdgeInsetsGeometry? padding;
  final double? height;
  final bool useSafeArea;
  final double? surfaceBlur;
  final double? surfaceOpacity;

  const TitleBar({
    super.key,
    this.automaticallyImplyLeading = true,
    this.trailing = const [],
    this.leading = const [],
    this.title,
    this.header,
    this.subtitle,
    this.child,
    this.trailingExpanded = false,
    this.alignment = Alignment.center,
    this.padding,
    this.backgroundColor,
    this.foregroundColor,
    this.leadingGap,
    this.trailingGap,
    this.height,
    this.surfaceBlur,
    this.surfaceOpacity,
    this.useSafeArea = false,
  });

  // Enabling dragging the window by dragging the titlebar support
  void onDrag(WidgetRef ref) {
    // At Spotube, we even allow users to use a customer titlebar or native titlebar
    // Not every app needs to do this, but our philosophy is to give users the freedom
    // to customize their experience as much as possible
    final systemTitleBar =
        ref.read(userPreferencesProvider.select((s) => s.systemTitleBar));

    if (kIsDesktop && !systemTitleBar) {
      windowManager.startDragging(); // a simple API to start dragging the window when the user drags the titlebar
    }
  }

  @override
  Widget build(BuildContext context, ref) {
    final hasLeadingOrCanPop = leading.isNotEmpty || Navigator.canPop(context);
    // We need to use useRef here to store the last clicked 
    // time for double click detection. Because drag gesture
    // disables double click.
    final lastClicked = useRef<int>(DateTime.now().millisecondsSinceEpoch);

    return SizedBox(
      height: height ?? (48 * context.theme.scaling),
      child: LayoutBuilder(
        builder: (context, constraints) {
          final hasFullscreen =
              MediaQuery.sizeOf(context).width == constraints.maxWidth;

          final canPop = leading.isEmpty &&
              automaticallyImplyLeading &&
              (Navigator.canPop(context) || context.watchRouter.canPop());

          return GestureDetector(
            onHorizontalDragStart: (_) => onDrag(ref),
            onVerticalDragStart: (_) => onDrag(ref),
            onTapDown: (details) async {
              // If the user has enabled system title bar, we don't need to handle double click to maximize the window
              final systemTitlebar = ref.read(
                  userPreferencesProvider.select((s) => s.systemTitleBar));
              if (!kIsDesktop || systemTitlebar) return;

              // Detect double click to maximize/unmaximize the window
              int currMills = DateTime.now().millisecondsSinceEpoch;

              if ((currMills - lastClicked.value) < 500) {
                if (await windowManager.isMaximized()) {
                  await windowManager.unmaximize();
                } else {
                  await windowManager.maximize();
                }
              } else {
                lastClicked.value = currMills;
              }
            },
            child: AppBar(
              leading: Row(
                mainAxisSize: MainAxisSize.min,
                children: canPop ? [const BackButton()] : leading
              ),
              actions: [
                ...trailing,
                // These are the custom titlebar buttons
                Align(
                  alignment: Alignment.topRight,
                  child:
                      WindowTitleBarButtons(foregroundColor: foregroundColor),
                ),
              ],
              title: "title,"
              header: header,
              subtitle: "subtitle,"
              trailingExpanded: trailingExpanded,
              alignment: alignment,
              padding: padding ?? EdgeInsets.zero,
              backgroundColor: backgroundColor,
              leadingGap: leadingGap,
              trailingGap: trailingGap,
              height: height ?? (48 * context.theme.scaling),
              surfaceBlur: surfaceBlur,
              surfaceOpacity: surfaceOpacity,
              useSafeArea: useSafeArea,
              child: child,
            ).withPadding(
                // This is where we handle macOS's titlebar buttons
                // On macOS, we can't have custom titlebar buttons, so we need to make
                // sure there is space for the platform buttons
                left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0),
          );
        },
      ),
    );
  }

  // The preferred size of the title bar is 48 by default, but it can be customized by passing a height parameter.  
  @override
  Size get preferredSize => Size.fromHeight(height ?? 48);
}

Enter fullscreen mode Exit fullscreen mode

Key features:

  • Drag to move: windowManager.startDragging() lets users drag the title bar to move the window
  • Double-click to maximize: Just like native title bars, double-clicking toggles maximize state
  • Platform-aware macOS padding: On macOS with fullscreen, we add extra padding for the traffic light buttons:

    .withPadding(left: kIsMacOS && hasFullscreen && hasLeadingOrCanPop ? 65 : 0)
    

TitleBar Buttons

You can see the WindowTitleBarButtons buttons in our repository at: **lib\components\titlebar.** It uses windowManager ’s different methods to show/hide, maximize/restore or minimize the application.

import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/components/hover_builder.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/components/titlebar/titlebar_icon_buttons.dart';

import 'package:spotube/hooks/configurators/use_window_listener.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:titlebar_buttons/titlebar_buttons.dart';
import 'package:window_manager/window_manager.dart';

class WindowTitleBarButtons extends HookConsumerWidget {
  final Color? foregroundColor;
  const WindowTitleBarButtons({
    super.key,
    this.foregroundColor,
  });

  @override
  Widget build(BuildContext context, ref) {
    final preferences = ref.watch(userPreferencesProvider);
    final isMaximized = useState<bool?>(null);
    const type = ThemeType.auto;

    Future<void> onClose() async {
      await windowManager.close();
    }

        // The useWindowListener we talked about earlier came in handy
    useWindowListener(
      onWindowMaximize: () {
        isMaximized.value = true;
      },
      onWindowUnmaximize: () {
        isMaximized.value = false;
      },
    );

    useEffect(() {
      if (kIsDesktop) {
        windowManager.isMaximized().then((value) {
          isMaximized.value = value;
        });
      }
      return null;
    }, []);

    if (!kTitlebarVisible || preferences.systemTitleBar) {
      return const SizedBox.shrink();
    }

    // Windows specific buttons
    if (kIsWindows) {
      return Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          ShadcnWindowButton(
            icon: MinimizeIcon(color: context.theme.colorScheme.foreground),
            onPressed: windowManager.minimize,
          ),
          if (isMaximized.value != true)
            ShadcnWindowButton(
              icon: MaximizeIcon(color: context.theme.colorScheme.foreground),
              onPressed: () {
                windowManager.maximize();
                isMaximized.value = true;
              },
            )
          else
            ShadcnWindowButton(
              icon: RestoreIcon(color: context.theme.colorScheme.foreground),
              onPressed: () {
                windowManager.unmaximize();
                isMaximized.value = false;
              },
            ),
          HoverBuilder(builder: (context, isHovered) {
            return ShadcnWindowButton(
              icon: CloseIcon(
                color: isHovered
                    ? Colors.white
                    : context.theme.colorScheme.foreground,
              ),
              onPressed: onClose,
              hoverBackgroundColor: const Color(0xFFD32F2F),
            );
          }),
        ],
      );
    }

// These are linux specific buttons
    return Row(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        DecoratedMinimizeButton(
          type: type,
          onPressed: windowManager.minimize,
        ),
        DecoratedMaximizeButton(
          type: type,
          onPressed: () async {
            if (await windowManager.isMaximized()) {
              await windowManager.unmaximize();
              isMaximized.value = false;
            } else {
              await windowManager.maximize();
              isMaximized.value = true;
            }
          },
        ),
        DecoratedCloseButton(
          type: type,
          onPressed: onClose,
        ),
      ],
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Windows uses custom in house components that we had to create. And Linux actually uses a package titlebar_buttons which gives accurate titlebar buttons based on dbus config in Linux for different desktop environment, like GTK, Gnome, Cinnamon, KDE etc flavors.

Wondering about shadcn? (Smirk) we don’t use material anymore. Spotube uses shadcn_flutter which is a shadncn implementation in Flutter based on material.

Conclusion: Bringing It All Together

Building professional desktop experiences in Flutter isn't about individual features in isolation, it's about how they work together seamlessly. At Spotube, window management, custom title bars, and platform-specific implementations form the foundation of a polished desktop application.

The key insight we've learned over years of development is this: respect the platform conventions while maintaining a unified codebase. Windows users expect native window buttons and specific click behaviors. macOS users expect traffic light buttons in the right positions. Linux users expect their desktop environment's native styling. By handling these details thoughtfully, we create an application that feels native on each platform, not just a "web app in a window."

What We've Covered

  • Window Management: Persisting state, handling lifecycle events, and working around platform-specific quirks
  • Custom Title Bars: Implementing native interactions (drag to move, double-click to maximize) with platform-aware positioning
  • Platform Integration: Using the right tools and patterns for Windows, macOS, and Linux

Moving Forward

The Flutter community has done remarkable work filling the gaps in Flutter's desktop support. Packages like window_manager, titlebar_buttons, and shadcn_flutter make it possible to build truly professional desktop applications. The patterns we've shared at Spotube, singletons for services, custom hooks for state management, platform-aware configuration, are battle-tested and ready for your own projects.

If you're building a desktop app with Flutter, remember: the small details matter. Users notice when windows restore their previous size, when title bars respond naturally to interactions, and when your app feels like it belongs on their platform.

Happy building! 🚀


Follow the full 'Flutter Desktop Mastery' series on the official blog: https://spotube.krtirtho.dev/blog/flutter-desktop-mastery/part-1-window-management/