Desktop app development with flutter

Alex SinelnikovAlex Sinelnikov
3 min read

For a long time desktop app development was hard and expensive. If you were creating app, you probably had to create 3 apps at once - windows, macOS and linux. There were tools such as QT, wxWidgets, copperspice and other frameworks which allowed to build cross platform apps. They worked but until some extent. Some of them were missing features or was really complex in terms on implementing UI. Eventually there was Electron.js which had huge impact on creating desktop apps. Now you could really just using html/css/js implement pretty much any app. And many companies just dropped their web versions into electron making it desktop. That’s why you can use web version and then switch to same desktop using Slack. However because its made in html/css it usually lacks fluid animations, and generally UX feel more like its browser than a native app.

With flutter and you can build really performance apps thanks to the way flutter renders contents. Recently I’ve created a cross platform app and open sourced it - [http://github.com/avdept/JellyBoxPlayer] The app runs on iOS/android but also on macOS. Windows and linux version still in works as it miss 1 feature that macOS has. And now a bit of my experience using flutter to create both mobile and desktop app.

Originally idea was to have just mobile app, but having iPad design I decided to add desktop support. I only had few minor changes to do and this is the result -

Tools and packages Turned out there’s only one 3rd party package I used - https://pub.dev/packages/responsive_builder which provides you a tools to check what kind of device you run it on, screen breakpoints, etc. So my code started to look like following

ScrollablePageScaffold(
      useGradientBackground: true,
      navigationBar: PreferredSize(
        preferredSize: Size.fromHeight(_isMobile ? 60 : 100),
        child: Padding(
          padding: EdgeInsets.symmetric(horizontal: _isMobile ? 16 : 30),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              _pageViewToggle(),
              _filterButton(),
            ],
          ),
        ),
      ),
      loadMoreData: loadMore,
      contentPadding: EdgeInsets.only(
        left: _isMobile ? 16 : 30,
        right: _isMobile ? 16 : 30,
        bottom: 30,
      ),
)

Depending on if it’s mobile or desktop I was able to use different paddings. Another case - hide/show element completely depending on platform

Visibility(
  visible: _isDesktop,
  child: CustomNavigationRail()
 )

One of the issues I had - flutter doesn’t allow setting min size for window when on desktop platforms. To fix this I found a window manager package and came up with this solution:

if (Platform.isMacOS || Platform.isLinux || Platform.isWindows) {
    await windowManager.ensureInitialized();

    const windowOptions = WindowOptions(
      size: Size(1440, 1000),
      minimumSize: Size(1280, 800),
      center: true,
      backgroundColor: Colors.transparent,
      skipTaskbar: false,
      titleBarStyle: TitleBarStyle.hidden,
    );

    await windowManager.waitUntilReadyToShow(
      windowOptions,
      () async {
        await windowManager.show();
        await windowManager.focus();
      },
    );
  }

The idea here to set size and also min size, but also prevent flickering when app starts, for this you got to wait until window is ready and then you can show and focus it.

The missing tools

Unfortunately flutter doesn’t know how to work with global keybindings. In my case it was media keys. There aren’t many available packages so I created my own. Since my main platform is macOS I started with it. I found MediaKeyTap swift package and based on that build flutter plugin. Main functions looks like below.

public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "mediakeys_proxy", binaryMessenger: registrar.messenger)
    let instance = MediakeysProxyPlugin(channel: channel)
    registrar.addMethodCallDelegate(instance, channel: channel)
    instance.startMonitoring()
    let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String : true]
    AXIsProcessTrustedWithOptions(options)
  }

  public func handle(mediaKey: MediaKey, event: KeyEvent) {

    switch mediaKey {
    case .playPause:
        self.sendEventToFlutter(eventName: "playPause")
    case .previous, .rewind:
        self.sendEventToFlutter(eventName: "prev")
    case .next, .fastForward:
        self.sendEventToFlutter(eventName: "next")
    }
  }

Conclusion

Over last few years flutter became really mature framework. Out of box it covers 90% possible use cases and it’s possible to use it without any 3rd party packages, but they surely make some things easier. Desktop platform still missing few features, but because of how flutter plugins works its really easy to come up with custom plugin solution to solve your problem.

Follow me on X(Twitter)

5
Subscribe to my newsletter

Read articles from Alex Sinelnikov directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Alex Sinelnikov
Alex Sinelnikov

Software engineer in my soul. Been building since early 2000.