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

推荐订阅源

F
Fortinet All Blogs
S
Secure Thoughts
月光博客
月光博客
美团技术团队
雷峰网
雷峰网
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
W
WeLiveSecurity
P
Proofpoint News Feed
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AI
AI
Last Week in AI
Last Week in AI
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
云风的 BLOG
云风的 BLOG
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recent Announcements
Recent Announcements
Webroot Blog
Webroot Blog
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
N
News and Events Feed by Topic
罗磊的独立博客
The Register - Security
The Register - Security
Blog — PlanetScale
Blog — PlanetScale
T
Threat Research - Cisco Blogs
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
人人都是产品经理
人人都是产品经理
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
B
Blog
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Engineering at Meta
Engineering at Meta
Latest news
Latest news
IT之家
IT之家
D
DataBreaches.Net
博客园 - 司徒正美
N
Netflix TechBlog - Medium
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

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 2: System Tray & Local Notifications
Kingkor Roy · 2026-05-16 · via DEV Community

The system tray is essential for a lot of desktop apps that needs to run in the background. But due to Flutter’s limited desktop support, again the Flutter Desktop Community has saved us with another much needed plugin.

At Spotube, users expect to control playback from the tray, minimize the app while keeping it running, and have quick access to essential features without opening the main window.

So, we implement a reactive system tray that stays in sync with the app's state. We use the tray_manager package to accomplish that.

Introducing the Dependencies

In Spotube's pubspec.yaml, we include:

dependencies:
  tray_manager: <latest>
  local_notifier: <latest>
  # we use it for state management
  hooks_riverpod: <latest>
  window_manager: <latest>

Enter fullscreen mode Exit fullscreen mode

The tray_manager package provides cross-platform system tray integration, while local_notifier handles desktop notifications.

Building the Tray Menu

Our tray menu is built reactively using Riverpod providers.

In lib/provider/tray_manager/tray_menu.dart:

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';

final trayMenuProvider = Provider((ref) {
//..... internal logic
// ref.watch() to other providers that update this
// provider on change

// This is the part that you need to understand.
  return Menu(
    items: [
      MenuItem(
        label: "Show/Hide Window",
        onClick: (menuItem) async {
          // We use window_manager plugin to show or hide the window
          if (await windowManager.isVisible()) {
            await windowManager.hide();
          } else {
            await windowManager.focus();
            await windowManager.show();
          }
        },
      ),
      MenuItem.separator(), // A special type of menu item that creates a divider between other menu items
      // Play/Pause button that adapts to current state
      MenuItem(
        label: isPlaying ? "Pause" : "Play",
        // You can disable (gray out) an option in the menu
        disabled: !isPlaybackPlaying,
        onClick: (menuItem) async {
          // .. we are handling play/pause logic here
        },
      ),
      MenuItem(
        label: "Next",
        disabled: !isPlaybackPlaying,
        onClick: (menuItem) {
          // ... next handle logic
        },
      ),
      MenuItem(
        label: "Previous",
        disabled: !isPlaybackPlaying,
        onClick: (menuItem) {
          // ... previous handle logic
        },
      ),
      // Submenu for advanced playback controls
      MenuItem.submenu(
        label: "Playback",
        submenu: Menu(
          items: [
            // Repeat mode toggle
            MenuItem(
              label: "Repeat",
              // Checked is a special boolean that can be used
              // to show a check next to an item in the menu
              checked: isLoopOne,
              onClick: (menuItem) {
                // Logic for repeat
              },
            ),
            // Shuffle toggle
            MenuItem(
              label: "Shuffle",
              checked: isShuffled,
              onClick: (menuItem) {
                // Logic for shuffle
              },
            ),
            MenuItem.separator(),
            MenuItem(
              label: "Stop",
              onClick: (menuItem) {
                // Logic for stop
              },
            ),
          ],
        ),
      ),
      MenuItem.separator(),
      MenuItem(
        label: "Quit",
        onClick: (menuItem) {
          exit(0); // Closes the program
        },
      ),
    ],
  );
});

Enter fullscreen mode Exit fullscreen mode

Key features of our tray menu:

  • Reactive state: Menu items update when playback state changes
  • Context-aware labels: "Play" vs "Pause" adapts to current state
  • Disabled items: Playback controls are disabled when no track is loaded
  • Checked items: Repeat and Shuffle show their current state with checkmarks
  • Organized submenu: Advanced controls grouped under "Playback"

Platform-Aware Tray Manager

The system tray behaves differently on Windows vs macOS/Linux.

In lib/provider/tray_manager/tray_manager.dart:

class SystemTrayManager with TrayListener {
  final Ref ref;
  final bool enabled;

  SystemTrayManager(
    this.ref, {
    required this.enabled,
  }) {
    initialize();
  }

  Future<void> initialize() async {
    if (!kIsDesktop) return;

    if (enabled) {
      // Set platform-specific tray icon
      await trayManager.setIcon(
        kIsWindows
            ? 'assets/branding/spotube-logo.ico'  // .ico for Windows
            : kIsFlatpak
                ? 'com.github.KRTirtho.Spotube'  // App ID for Flatpak
                : 'assets/branding/spotube-logo.png',  // PNG for others
      );
      // Register this object as a listener for tray events
      trayManager.addListener(this);
    } else {
      // Clean up when tray is disabled
      await trayManager.destroy();
    }
  }

  void dispose() {
    trayManager.removeListener(this);
  }

  // Handle left-click on tray icon
  @override
  onTrayIconMouseDown() {
    if (kIsWindows) {
      // Windows convention: left-click shows the window
      windowManager.show();
    } else {
      // macOS/Linux convention: left-click shows context menu
      trayManager.popUpContextMenu();
    }
  }

  // Handle right-click on tray icon
  @override
  onTrayIconRightMouseDown() {
    if (!kIsWindows) {
      // macOS/Linux: right-click shows the window
      windowManager.show();
    } else {
      // Windows: right-click shows context menu
      trayManager.popUpContextMenu();
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Platform differences we handle:

  • Icon format: Windows requires .ico, Linux/Flatpak use PNG or app ID strings
  • Click behavior: Windows expects left-click to open, macOS/Linux expects right-click
  • Listener pattern: We implement the TrayListener interface to receive tray events

The Tray Manager Provider

Our Riverpod provider ties everything together:

final trayManagerProvider = Provider(
  (ref) {
    // Watch user preference for showing tray icon
    final enabled = ref.watch(
      userPreferencesProvider.select((s) => s.showSystemTrayIcon),
    );

    // Update tray menu reactively whenever app state changes
    ref.listen(trayMenuProvider, (_, menu) {
      if (!enabled || !kIsDesktop) return;
      trayManager.setContextMenu(menu);
    });

    // Create and manage the tray manager instance
    final manager = SystemTrayManager(
      ref,
      enabled: enabled,
    );

    // Clean up when provider is disposed
    ref.onDispose(manager.dispose);

    return manager;
  },
);

Enter fullscreen mode Exit fullscreen mode

Why this pattern is powerful:

  • Reactive updates: When some state (for Spotube, audio) changes (e.g. play/pause), the menu updates automatically
  • User control: Users can toggle tray icon from settings
  • Lifecycle management: Proper cleanup with ref.onDispose
  • Single source of truth: Audio player state is the source, tray menu is computed from it

Initializing the Tray in Your App

In your root app widget, initialize the tray manager early:

class Spotube extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, ref) {
    // Initialize tray manager
    ref.listen(trayManagerProvider, (_, __) {});

    // ... rest of your app
  }
}

Enter fullscreen mode Exit fullscreen mode


Desktop Notifications 🔔

Desktop notifications inform users about important events even when the app window is hidden or minimized. At Spotube, we use the local_notifier package to show native desktop notifications.

Initializing Local Notifications

In your lib/main.dart, initialize the notification system for desktop:

Future<void> main(List<String> rawArgs) async {
  final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();

  if (kIsDesktop) {
    // Initialize local notifier with app name
    // This name appears in system notification settings
    await localNotifier.setup(appName: "Spotube");

    // Initialize other desktop services
    await WindowManagerTools.initialize();
  }

  // ... rest of initialization
}

Enter fullscreen mode Exit fullscreen mode

The Minimize to Tray Notification

When users minimize Spotube to the system tray, we show a notification to confirm the app is still running. This is handled in lib/hooks/configurators/use_close_behavior.dart:

import 'package:local_notifier/local_notifier.dart';

// This notification is created once and reused multiple times
final closeNotification = !kIsDesktop
    ? null
    : (LocalNotification(
        title: "'Spotube',"
        body: 'Running in background. Minimized to System Tray',
        actions: [
          LocalNotificationAction(text: 'Close The App'),
        ],
      )..onClickAction = (value) {
        // User clicked "Close The App" action
        exit(0);
      });

Enter fullscreen mode Exit fullscreen mode

Breaking this down:

  • Desktop-only: The notification is only created on desktop platforms
  • Nullable: On mobile, it's null, preventing crashes
  • Callback: onClickAction is invoked when the user clicks the notification action
  • Reusable: We create it once and call show() multiple times

Showing the Notification

When the user closes the window and has "minimize to tray" enabled:

void useCloseBehavior(WidgetRef ref) {
  useWindowListener(
    onWindowClose: () async {
      final preferences = ref.read(userPreferencesProvider);

      if (preferences.closeBehavior == CloseBehavior.minimizeToTray) {
        // Hide the window
        await windowManager.hide();

        // Show notification that app is still running
        closeNotification?.show();
      } else {
        // Close completely
        exit(0);
      }
    },
  );
}

Enter fullscreen mode Exit fullscreen mode

This notification gives users confidence that their music will keep playing and shows them how to close the app if needed.

Are you curious about this weird way of writing logical function? These look similar to react-hook, aren’t they? Actually, at Spotube, to reduce boilerplate and easier life-cycle handling of widgets, we use flutter_hooks package that is basically react-hooks but for Flutter. You can learn about the custom useWindowListener hook on part 1 of this series.

Building Custom Notifications

You can create notifications for other scenarios. Here's a pattern for doing it safely:

void showTrackChangeNotification(Track track) {
  if (!kIsDesktop) return;  // Only on desktop

  final notification = LocalNotification(
    title: "'Now Playing',"
    body: '${track.name}${track.artist}',
    silent: false,  // Play system sound
  );

  notification.show();
}

void showDownloadCompleteNotification(String fileName) {
  if (!kIsDesktop) return;

  final notification = LocalNotification(
    title: "'Download Complete',"
    body: fileName,
    actions: [
      LocalNotificationAction(text: 'Open Folder'),
    ],
  );

  notification.onClickAction = (value) {
    // Open the downloads folder
    openFileExplorer(downloadPath);
  };

  notification.show();
}

Enter fullscreen mode Exit fullscreen mode

Best practices:

  • Always check kIsDesktop: Prevents crashes on mobile
  • Provide meaningful titles and bodies: Users should understand what happened
  • Use actions sparingly: Notification actions are powerful but can be confusing
  • Avoid notification spam: Don't show notifications for every minor event

Putting It All Together: The Complete Desktop Experience

Here's how window management, system tray, and notifications work together in Spotube's initialization:

class MainApp extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, ref) {
    // Make sure run this in the root app to Setup system tray with reactive menu
    // Riverpod providers are lazy, so we need to listen to it here to make sure it's initialized
    ref.listen(trayManagerProvider, (_, __) {});

    return ShadcnApp.router(
      // ... app configuration
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

The user flow:

  1. User clicks close buttonuseCloseBehavior intercepts
  2. Close preference checked → If "minimize to tray" is enabled...
  3. Window hideswindowManager.hide()
  4. Notification shows → User sees "Running in background"
  5. User interacts with tray → Platform-specific behavior triggers
    • Windows: Left-click shows window
    • macOS/Linux: Right-click shows window
  6. Tray menu displays → Reactive menu with current playback state
  7. User controls playback → Menu items trigger audio player actions
  8. Menu updates → State changes reflect in next menu open

This seamless integration is what makes Spotube feel like a native desktop application.

Happy building! 🚀


Follow the full 'Flutter Desktop Mastery' series on the official blog: https://spotube.krtirtho.dev/blog/flutter-desktop-mastery/part-2-system-tray-local-notifications/