# Flutter on Desktop: What Broke on Windows, macOS and Linux

> Shipping one Flutter game to three desktop systems taught us where the code really differs. Video, store packaging, rendering and window size, with the fixes.

By Alexandr Bykov, OxiSoft. Published 15 October 2026.
Canonical: https://oxisoft.io/blog/flutter-desktop-in-production/

"Write once, run anywhere" is mostly true for Flutter. Our typing game,
[OxiType](/apps/oxitype/), is one Dart codebase that runs on macOS, Windows and
Linux, and well over ninety percent of the code has no idea which system it is
on. The game loop, the levels, the bosses, the scoring: all shared.

The last few percent is where the time went. Nothing in this list is exotic,
and none of it showed up until we built and ran on the real system. If you are about to take a Flutter app to
desktop, this is the list we wish we had.

## Video: the official plugin doesn't cover every desktop

The main menu plays a short looping video behind the buttons. On macOS it
worked on day one with the standard video plugin from the Flutter team.

On Windows there was no video. The reason is simple once you look:
that plugin supports Android, iOS, macOS and the web. It has no Windows or Linux
implementation, so on those systems the player never gets ready.

We didn't want to throw away a plugin that works well where it works, so the
menu now picks a player per platform. Windows uses a community player built on
a cross-platform media engine; everything else keeps the official one. The
menu code asks one question ("am I on Windows?"), and the rest of the screen
does not care which player it got.

Then came the part worth remembering. The new player needs a one-time setup
call when the app starts. We added it at the top of the app, for everyone,
and the macOS build broke. The setup belongs only to the platform that uses
the player:

```dart
if (Platform.isWindows) {
  MediaKit.ensureInitialized();
}
```

**Rule we follow now:** any plugin added for one platform gets its setup
behind a platform check, in the same commit. A "harmless" global init is
how a Windows fix becomes a macOS bug.

## Windows: the store tells you who you are

Flutter can build a Windows package for the Microsoft Store, and the build
itself went fine. The Store did not agree with what was inside it.

Every Windows package has an identity name and a publisher ID. Like most
mobile developers, we filled the identity in with our usual reverse-domain
style. The Store doesn't accept an identity you invent. When you reserve the
app name, it gives you the exact identity and publisher strings, and the
package must match them character for character.

The fix was two lines in the packaging config. Finding out which two lines
took longer. If you ship to the Microsoft Store, copy both values from the
Store's product page before your first build, and keep the package version in
step with the app version. We bump both together on every release, so they
never drift apart.

## macOS: a framework with the wrong shape

macOS frameworks are folders with a strict layout. The real files live in a
versioned subfolder, and the top level holds links that point into it. Most
of the time this is invisible.

After a dependency update, one framework pulled in by a plugin arrived in the
app with some of those top-level links missing. The app ran fine on our Macs.
App Store validation flagged it as a malformed bundle.

We fixed it with a small build step that runs after the frameworks are
embedded: if the expected links are missing, it creates them. It is a dull
fix, and it holds. The lesson is not about that one framework. **"It runs on
my Mac" says nothing about the App Store.** Run the store's validation on every
release build before you plan the release date, not after.

## Linux: when the new renderer lags

Flutter has two rendering engines. Skia is the old one. Impeller is the new
one, built to remove the stutter that shader compilation used to cause, and
it's now the default on more and more platforms.

On Linux, our game lagged badly. The same build on macOS was smooth, so this
was not a slow game loop. The cause was the glow. The ships, the eighteen
bosses and the buttons all carry a neon glow drawn with a blur filter, many
times per frame. On Linux, Impeller's OpenGL backend stalled on exactly that kind of
drawing.

The fix is one line in the Linux runner that turns Impeller off for that
platform and uses Skia instead. The game was smooth again in version 1.0.13.
The other platforms keep their default renderer.

Two notes if you hit the same thing:

- **Test the effect, not the app.** A plain list screen will not show this.
  Heavy blur, many layers and lots of redraws will. Build a stress scene of
  your worst effect and run it on each platform.
- **Write down why you switched.** We left a comment next to that line naming
  the effect that caused it. When a future Flutter release fixes Impeller on
  Linux, we want to know what to re-test before we flip it back.

## Windows are not phones

The last group is less about bugs and more about habits from mobile.

A phone gives you a screen of a known shape. A desktop gives you a window that
can be any size, on a monitor of any size. Our game is designed for a 4:3
playing field, so at start we size the window from the real screen: two thirds
of the screen height, width from the aspect ratio, a floor so the game stays
playable on small laptops, and a ceiling so the window does not run under the
taskbar or the dock.

Desktop players also expect things phones never asked for: a keyboard path
through every menu (arrow keys, Enter, Escape), hotkeys, and a window that
opens focused so the first key press is not lost. For a typing game that last
one is not polish. If the window isn't focused, the game can't hear the keyboard.

## Was it worth it?

Yes. We shipped one game to three desktop systems with one small team, and
the platform code is a handful of short, well-marked spots. Each fix above is a
few lines. Finding them is the real cost, and the only way we found any of them
was to run the release build on the real system, early and often.

If you are choosing a stack for a cross-platform app, we explained our reasons
for Flutter in [Go, Flutter and Rust](/blog/why-we-use-go-flutter-rust/). The
short version still holds: one codebase, honest about where it has to split.
