Recently, my post on building Shortcut Keeper as a native-looking desktop app made the rounds on Hacker News. It even reached the top-20 for that day, which obviously brought a lot of traffic, as well as a wealth of feedback from readers.
Among this feedback, my approach of using Flutter for building a cross-platform desktop app drew some comments like this:
This is a valid concern, of course. Many frameworks in the past have promised to bring cross-platform capabilities, matching native implementations, from a single codebase. It’s no easy feat; each platform has its nuances and quirks to take into account.
So, in this post, my main point will be that it is not hard to work with Flutter and a single codebase for both desktop platforms.
It is indeed possible to have close to native performance and an adaptive UI (as shown in the previous post) on both macOS and Windows, without the extra overhead of using two different programming languages, frameworks, and tooling for each one.
To bolster my point, I will also describe how I implemented a number of new app features for the app’s latest update (version 1.3.0), first for the macOS version and then for Windows.
Breaking down of the app
Shortcut Keeper is a rather simple app. It was developed in a short timeframe and is meant to do one thing and do it well: save and keep the user’s favorite keyboard shortcuts.
To get a sense of the app’s scope, it would help to provide a quick top-level view of its architecture. This will also help in reasoning about what stays the same, and what needs to be adjusted between the two versions, with each update.
So, let’s see how it can be broken down to its main components.
This is the core of any app and takes most of the development time by far. It includes everything about shortcuts (recording, creating, updating, storing, formatting, etc.), state management, app settings, error checking, serializing, and everything else that makes the app useful and functional in the first place.
Between macOS and Windows: stays mostly the same for both platforms. Some adjustments are necessary for:
a) handling differences with each platform’s keys (e.g. Command on macOS vs Control on Windows) and how they are handled and presented.
b) using platform channels for the global hotkey. These have to be calls to native C++ and Swift code to bring the app to the front.
The fact that this section is similar for both platforms is the main benefit of developing with a single codebase and Flutter. I don’t have to worry about using two different languages/frameworks to handle the main app features.
Check the following snippet, for example.
Reading data from a JSON file, creating new shortcut entries, adding them to the shortcuts list, updating the state of the UI, and handling potential errors, are all implemented the exact same way for both platforms:
As a single developer, I can’t even imagine, for example, having to learn to manage state or dealing with file I/O in two different environments, like UWP and Cocoa.
The app uses a SQLite database to store the saved shortcuts locally, via the sqflite package, available for both platforms.
Between macOS and Windows: stays the same. The package abstracts all interactions with the database. So, I can just write:
and be sure that it will indeed insert the shortcut into theSQLite database on both macOS and Windows.
This includes things like the layout of the app, where buttons are placed, what icons are used, its color palette, and the general design and feel of the app.
Here, I could also have the exact same UI for both platforms with minimal changes. I instead went with a native-looking UI for each one, as explained in more detail in the previous post.
In fact, when I first tried to port the existing macOS version of the app into Windows, this was the result:
I thought that this could look a bit out of place for Windows users. So, I went ahead and built the app with adaptability, trying to match the look and norms of the underlying platform:
Between macOS and Windows: different in my case, but could be the same if I preferred to use an identical custom design for both platforms. An example of this, would be an app like Spotify or Android Studio, which have almost exactly the same UI design on all their different platform versions.
Building, signing, and deploying the final executable for release is, of course, different for each version.
Each platform requires a different executable (.exe and .app) and, if you deploy through the app stores, a specific process to submit it for review. However, there is the ability to use CI/CD tooling for Flutter desktop with a solution like Codemagic, to streamline this to a degree.
Between macOS and Windows: different. This is naturally the case with every cross-platform or native framework, but can be alleviated using a CI/CD tool.
There are of course more features that a desktop app can typically have. Below are some examples, which have a limited or non-existent use for Shortcut Keeper, but can surely be implemented using Flutter desktop:
- Making network requests. Shortcut Keeper only launches a few links from the app, but making requests like calls to an API would be consistent between platforms.
- Drawing on a graphical interface (like a canvas). Since Flutter works similar to a game engine to draw on the platform, it would be as simple as using a Canvas widget, to provide features like grabbing a signature or making a drawing app.
- Handling QR codes.
- Crafting custom complex animations.
A developer can implement such features cross-platform with Flutter and its third-party package support, without the added overhead of learning how to do it one way for macOS and another for Windows.
Now, let’s see what was included in the recent update (version 1.3.0) of Shortcut Keeper.
The app update
Along with a few UI improvements and fixes, the latest version of Shortcut Keeper includes:
- The ability to use keyboard shortcuts in Shortcut Keeper, like ⌘ N (Control+N) to add a new shortcut. A new menu bar menu was also added for macOS.
- The ability to quickly duplicate a shortcut (⌘D or Control+D).
- The ability to select multiple shortcuts and delete them (D or Del) at once.
- The option to add shortcuts that have two steps when adding a new shortcut (“chord” shortcuts). An example would be VS Code’s ⌘K ⌘S to select the app’s color theme.
Building the new update’s features on macOS
Since my main development machine is an iMac, I first worked on the macOS version of the app, and then moved to Windows.
As expected, the above features touched nearly every aspect of the app’s codebase.
For example, to add the ability to select multiple shortcuts and delete them at once, I had to:
- Add a button in the top bar to toggle the multi-select mode (affecting the UI design).
- When in multi-select mode, add boxes with the appropriate icon and click behavior next to every shortcut (affecting the UI and logic).
- Implement what happens when the user clicks on these boxes and selects shortcuts (logic/state management).
- Set which actions should be available when one or multiple shortcuts are selected. For example, it’s possible to edit a single shortcut, but not multiple. However, a user can delete both a single and multiple shortcuts (UI and logic/state management).
- Implement deleting multiple shortcuts (logic and database calls).
- Write integration tests and check that it works.
Evidently, this took most of the planning and development time. Thankfully, Flutter’s hot reload and IDE integration are really helpful in speeding up the process.
In the end, building all these features took me around 5-6 days. I then compiled the release executable of the macOS app, submitted it to the App Store for review, and went on with porting the same features to the Windows version.
Moving to the Windows version
Continuing on my Windows laptop, I pulled the latest changes from my app’s git repo and started work on updating the app with the same features for version 1.3.0.
Here is where the benefits of using Flutter for desktop development truly shine:
I was able to get all the new features and fixes ready for Windows deployment in ~2 hours of work!
For example, to get the multi-select mode I described above, working on the Windows version, I had to:
- Insert the toggle button in the widget tree for Windows.
- Write its Windows implementation, which is similar to the macOS one, but with different icons and a slightly smaller size:
- Select which icons to use for the boxes next to the shortcuts. I went with a rectangular shape in the Windows version, in contrast to the rounded macOS ones, to match its design:
- Checked and tested how it looks for both dark and light themes.
That’s it! Since the whole logic of the multi-select feature is handled exactly the same both in macOS and Windows, I didn’t have to do anything else. No time was spent on adjusting database calls, or the state management of the top bar and the main screen.
A similar process was used for the rest of the app updates:
- Duplicating a shortcut: worked immediately.
- Recording “chord” shortcuts: worked immediately, as I used the already existing
PlatformCheckboxfor the UI. Only needed to adapt how it is labeled on Windows (shortcuts are delimited by the “+” symbol, instead of “-“).
- Enabling keyboard shortcuts for the app (like Control+N): worked immediately. I opted to not have a menu bar in the Windows version at all.
After some sanity checks and testing, I was able to build the MSIX installer and submit it to the Microsoft Store in a couple of hours.
Of course, someone could say that I could achieve the same result with other cross-platform frameworks, like Electron, Swing, or React Native. I touched upon why I chose to use Flutter to build Shortcut Keeper in this post.
In the end, I was able to build the same features of the macOS version and ship them for the Windows one with minimal hassle.
It could also be a lot quicker if I didn’t strive to have an adaptive UI for each platform (if I went with the default Material UI that Flutter provides, that is). Since the app’s logic, which accounts for most of the development effort, is pretty much the same, it eventually comes down to just a few adjustments to the UI.
I truly believe Flutter desktop is mature enough to build production apps with high quality.
As shown here, it enabled me, a developer with no prior development experience on desktop, to build on my own a desktop app for macOS and Windows (Linux is also possible).
I would not be surprised to see bigger teams, with more experience and resources, use Flutter for desktop and achieve similar results.
Thanks for reaching the end of this post! Here goes my cat again: