Flutter on Top: Elevate Your App to the macOS Status Bar

sky1095

Sudhanshu Kumar Yadav

Posted on May 16, 2023

Flutter on Top: Elevate Your App to the macOS Status Bar

Hello, fellow Flutter enthusiasts! 🎉

Today, we're going to dive into something truly magical. We're going to take a Flutter app and make it run in the macOS status bar. Yes, you read that right! We're going to take our Flutter prowess to the next level and make our apps even more accessible and user-friendly. So, buckle up, and let's get started! 🚀

Flutter Meets macOS Status Bar

First things first, let's talk about why you might want to run your Flutter app in the macOS status bar. The status bar is that nifty little area at the top of your screen that houses useful information and quick access to system functions. It's always visible, making it a prime real estate for apps that users need to access frequently.

Imagine having a mini weather app, a quick note-taking app, or a system monitor right there in the status bar. Sounds cool, right? Well, with Flutter, we can make it happen!

The Magic Begins

To get started, we need to set up our Flutter project. If you're new to Flutter, check out the official Flutter installation guide.

flutter create flutter_statusbar_app
cd flutter_statusbar_app
Enter fullscreen mode Exit fullscreen mode

Now, we're going to dig a lil deeper in our brewed flutter project, head to flutter_statusbar_app/macos/Runner/AppDelegate.swift file. You need to change it a bit with the below code:

import Cocoa
import FlutterMacOS

@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
  // Instance of the status bar controller
  var statusBarController: StatusBarController?

  // Instance of the popover that will display the Flutter UI
  var flutterUIPopover = NSPopover.init()

  // Initializer for the AppDelegate class
  override init() {
    // Set the popover behavior to transient, meaning it will close when the user clicks outside of it
    flutterUIPopover.behavior = NSPopover.Behavior.transient
  }

  // Function to determine whether the application should terminate when the last window is closed
  override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
    // Return false to keep the application running even if all windows are closed
    return false
  }

  // Function called when the application has finished launching
  override func applicationDidFinishLaunching(_ aNotification: Notification) {
    // Get the FlutterViewController from the main Flutter window
    let flutterViewController: FlutterViewController =
      mainFlutterWindow?.contentViewController as! FlutterViewController

    // Set the size of the popover
    flutterUIPopover.contentSize = NSSize(width: 360, height: 360) // Change this to your desired size

    // Set the content view controller for the popover to the FlutterViewController
    flutterUIPopover.contentViewController = flutterViewController

    // Initialize the status bar controller with the popover
    statusBarController = StatusBarController.init(flutterUIPopover)

    // Close the default Flutter window as the Flutter UI will be displayed in the popover
    mainFlutterWindow.close()

    // Call the superclass's applicationDidFinishLaunching function to perform any additional setup
    super.applicationDidFinishLaunching(aNotification)
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's break it down:

  1. Import Statements: The import Cocoa and import FlutterMacOS statements are importing the necessary libraries for the macOS application and Flutter.

  2. AppDelegate Class: The AppDelegate class is a subclass of FlutterAppDelegate. This class is the entry point of the application and handles application-level events.

  3. Variables: The statusBar variable is an instance of StatusBarController (not shown in this code snippet), which presumably manages the status bar item. The popover variable is an instance of NSPopover, which is a macOS UI component that displays content in a separate hovering window.

  4. init() Function: The init() function is the initializer for the AppDelegate class. Here, it sets the popover behavior to NSPopover.Behavior.transient, which means the popover will close when the user clicks outside of it.

  5. applicationShouldTerminateAfterLastWindowClosed(_:) Function: This function determines whether the application should terminate when the last window is closed. It returns false, meaning the application will keep running even if all windows are closed.

  6. applicationDidFinishLaunching(_:) Function: This function is called when the application has finished launching. It does several things:

  • It gets the FlutterViewController from the mainFlutterWindow, which is the main window of the Flutter application.
  • It sets the size of the popover and assigns its contentViewController to the FlutterViewController. This means the Flutter UI will be displayed in the popover.
  • It initializes the statusBar with the popover, presumably creating a status bar item that shows the popover when clicked.
  • It closes the mainFlutterWindow, as the Flutter UI will be displayed in the popover instead of the main window.
  • It calls super.applicationDidFinishLaunching(aNotification), which calls the same function in the superclass (FlutterAppDelegate), performing any additional necessary setup.

Now let's move to creating the StatusBarController.swift file:

For this open your macos folder in Xcode. Goto file at the top: File -> New -> File -> Choose Swift File

Create a StatusBarController.swift file

N.B.: don't try to use shortcut and create this file from other code editor like VSCode.

add the following magical code in the created file

import AppKit

class StatusBarController {
    // Instance of the status bar
    private var appStatusBar: NSStatusBar

    // Instance of the status bar item
    private var statusBarMenuItem: NSStatusItem

    // Instance of the popover that will display the Flutter UI
    private var flutterUIPopover: NSPopover

    // Initializer for the StatusBarController class
    init(_ popover: NSPopover) {
        self.flutterUIPopover = popover
        appStatusBar = NSStatusBar.init()
        statusBarMenuItem = appStatusBar.statusItem(withLength: 28.0)

        // Configure the status bar item's button
        if let statusBarMenuButton = statusBarMenuItem.button {
            // Set the button's image
            statusBarMenuButton.image = #imageLiteral(resourceName: "AppIcon") // Change this to your desired image
            statusBarMenuButton.image?.size = NSSize(width: 18.0, height: 18.0)
            statusBarMenuButton.image?.isTemplate = true

            // Set the button's action to toggle the popover when clicked
            statusBarMenuButton.action = #selector(togglePopover(sender:))
            statusBarMenuButton.target = self
        }
    }

    // Function to toggle the popover when the status bar item's button is clicked
    @objc func togglePopover(sender: AnyObject) {
        if(flutterUIPopover.isShown) {
            hidePopover(sender)
        }
        else {
            showPopover(sender)
        }
    }

    // Function to show the popover
    func showPopover(_ sender: AnyObject) {
        if let statusBarMenuButton = statusBarMenuItem.button {
            flutterUIPopover.show(relativeTo: statusBarMenuButton.bounds, of: statusBarMenuButton, preferredEdge: NSRectEdge.maxY)
        }
    }

    // Function to hide the popover
    func hidePopover(_ sender: AnyObject) {
        flutterUIPopover.performClose(sender)
    }
}
Enter fullscreen mode Exit fullscreen mode

Don't worry, we will break down this code blob too if it doesn't look familiar:

This Swift code defines a class StatusBarController that manages a status bar item for a macOS application. The status bar item is associated with a popover, which is a UI element that displays content in a separate window that appears above other content onscreen.

  1. Import Statement: import AppKit imports the necessary library for working with the macOS user interface.

  2. Class Definition: class StatusBarController defines the class. It has four properties: statusBar, statusItem, popover, and statusBarButton.

  3. Initializer: The init(_ popover: NSPopover) function is the initializer for the class. It takes an NSPopover as an argument and sets up the status bar and status bar item. The status bar item's button is configured with an image and an action that toggles the popover when clicked.

  4. togglePopover(sender: AnyObject) Function: This function is called when the status bar item's button is clicked. It checks if the popover is shown and calls either showPopover(_:) or hidePopover(_:) accordingly.

  5. showPopover(_ sender: AnyObject) Function: This function shows the popover. It positions the popover relative to the status bar item's button.

  6. hidePopover(_ sender: AnyObject) Function: This function hides the popover.

In summary, this class manages a status bar item that, when clicked, toggles a popover. The popover is positioned relative to the status bar item's button.


That's all for the native side, moving to dart side 🎯

Run your main.dart as macOS desktop target selected using;

flutter run -d macos 
Enter fullscreen mode Exit fullscreen mode

if you can't see macOS as an option in your available device list;
run this command once

flutter config --enable-macos-desktop 
Enter fullscreen mode Exit fullscreen mode

And voila! You've just created a Flutter app that runs in the macOS status bar! 🎉

Wrapping Up

Running a Flutter app in the macOS status bar opens up a world of possibilities. It allows you to create apps that are always accessible and provide real-time information or quick functionality. So go ahead, experiment, and create something amazing!

Checkout the repo for the complete project

💖 💪 🙅 🚩
sky1095
Sudhanshu Kumar Yadav

Posted on May 16, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related