Starling SDK · a cross-platform app framework

A new Swift GUI framework in town. It's Flutter's.

The whole Flutter framework — widgets, rendering, painting, gestures, animation, semantics — ported to Swift, running on the real Flutter engine. Same declarative model, same layout algorithm, same widget catalogue. No Dart VM, and no rewrite of the hard parts underneath.

The framework
Flutter's, ported line by line to Swift. Its concepts, its widget tree, its constraint-based layout.
The language
Swift. Real threads, C and C++ for free, ahead-of-time compiled, no VM in the process.
The engine
Flutter's own C++ engine, unmodified in shape — Skia, the text stack, the embedders.

Here to build something rather than read the argument? Start here — install, write an app, run it.

The idea

Two good things that had never met

Flutter's framework is one of the best pieces of UI design of the last decade. The declarative, React-style model where the tree is a function of state. Widgets as cheap, immutable descriptions with elements underneath doing the reconciling. And the layout engine — a single-pass, constraint-based algorithm where constraints go down and sizes come up, which is genuinely clever and gets you predictable layout in linear time. Fifteen years of accumulated design, and it works.

Swift is a wonderful language: fast, safe, expressive, ahead-of-time compiled, with real concurrency and an ownership story that keeps getting better. It is also open source and runs on Linux and Windows.

But if you write Swift and want a graphical application, your options thin out fast. SwiftUI is superb and closed — Apple's platforms only, no source, and nothing to port. Bindings to GTK or Qt exist and are useful, but they wrap imperative C and C++ toolkits; you get someone else's widgets and someone else's programming model, not a declarative one. Outside Apple's walls, a Swift developer who wants the model SwiftUI made everyone want has had very little to reach for.

Flutter has the framework Swift is missing. Swift has the properties Flutter's framework was never allowed to use. Putting them together has been an obvious idea for years — the work is in actually doing it.
What it is

Ported above, unchanged below

The dividing line is exactly where Flutter already draws it. Everything Dart did, Swift now does. Everything C++ did, C++ still does.

Swift
Your app — widgets, state, business logic
Swift
The framework — Widgets, Rendering, Painting, Gestures, Animation, Semantics, plus Material-style and macOS-style widget sets
Swift
dart:ui, in Swift — Offset, Size, Rect, Paint, Canvas, Paragraph: the same API surface, bridged to the engine's own implementation
C++
The Flutter engine — Skia and Impeller, the text stack (HarfBuzz shaping, ICU line breaking, font fallback), compositing, the frame pipeline, the platform embedders

There is no Dart VM anywhere in that stack. Where the engine would normally start a Dart isolate and call into it, it starts a Swift runtime delegate instead and calls into that. From the engine's point of view almost nothing changed, which is the property that makes the whole thing tractable.

The engine question

Why we kept the C++ engine

We are not the first to notice that Flutter's framework and Swift belong together. Shaft is a Swift port of Flutter's framework too, and a good project — it reached the same conclusions we did about what Swift brings, right down to naming native multithreading, direct graphics access and ARC as the reasons. Two independent projects converging on the same argument is worth more than either of us asserting it.

Where we differ is the layer below. Shaft replaces the engine with its own backend — Skia over SDL, with direct access to Metal, OpenGL and DirectX. That buys real things: a much smaller dependency, low-level graphics control, and multi-window support that falls out naturally. We took the other road and kept Flutter's engine, for three reasons.

01Text is the part nobody should rewrite

Drawing a rectangle is easy. Laying out a paragraph is not. Real text needs shaping through HarfBuzz, line breaking through ICU, bidirectional reordering, font fallback across scripts, emoji clusters, ellipsis, selection geometry and accessibility bounds — and it needs to be right in Arabic and Thai and Japanese, not only in English. Flutter's engine has years of work in that layer and ships it tuned. Inheriting it is the single largest thing keeping the engine buys.

02The embedders are the platform work

The engine already carries embedders for Android, iOS and macOS, Windows, Linux and more. Each one is the unglamorous, essential work of a platform port: window and surface management, input, IME, accessibility, vsync and frame scheduling. Our Linux desktop host is the engine's own GTK embedder, running in Swift mode — so window management and input come from the exact code path a real Flutter Linux app uses. Every embedder that exists is a platform we can reach without writing that layer ourselves.

03Upstream keeps improving without us

Impeller, rasterizer work, text fixes, platform updates — all of it lands in the engine, and all of it is ours by tracking a fork rather than by reimplementing. The framework is the part we wanted to own, because that is the part the language shows through. The renderer is the part we wanted to inherit.

The cost is honest: the engine is a large dependency, and we live with its architecture rather than around it. We think that is the right trade for a framework meant to run real applications on many platforms — but it is a trade, and Shaft's answer is a legitimate one for people who want the graphics layer in their own hands.

What Swift brings

The properties the framework gains

A port is only worth the effort if the new language gives you something the old one could not. Three things, in order of how much they mattered.

01Real threads that share memory

Dart's concurrency model is isolates: separate heaps, no shared mutable state, messages copied between them. That is a genuine feature — it makes data races structurally impossible — and for most app code it is the right trade. But some programs cannot pay it. A video pipeline, a compositor, an audio engine, anything moving large buffers between threads: a 4K frame crossing an isolate boundary as a copy is not a design, it is a budget you have already spent.

In Swift that is ordinary code — a lock, an atomic, a class shared between threads, with Sendable and actors when you want the compiler checking your work. To be precise about what did not change: the framework's element tree is still single-threaded, exactly as in Flutter. You build UI on one thread. The difference is that everything around the UI can be genuinely concurrent and still share memory with it.

02C and C++ at zero cost, with no bindings layer

Swift imports a C header as a module. You write import SomeCLibrary and call its functions like any other — no binding declarations, no code generator, no marshalling step, no second description of a struct you have already described in a header. Structs arrive as structs, function pointers as closures, and C++ interop is a build setting.

Dart reaches C through dart:ffi, which works, but is a bridge: bindings declared or generated, memory managed by hand through Pointer, and rules about which isolate a callback may run on. For an app calling one library that is a small tax. For anything built on a stack of native libraries it becomes most of the program — and native libraries are where the interesting capabilities live.

03No VM in the process

Swift compiles ahead of time to native code with reference counting: no interpreter, no JIT warm-up, and no tracing garbage collector deciding when to pause. Startup is a native binary starting. The honest version is that ARC is not free either — retain and release traffic costs something — but it is deterministic and visible in a profile, which is a different kind of problem from a pause you did not schedule.

Side by side

If you know Flutter, you already know this

Swift and Dart are close relatives — classes, generics, closures, named arguments, optionals, trailing closures. The port is a near-mechanical translation rather than a reimagining, which means Flutter's documentation, its widget catalogue and fifteen years of accumulated answers still apply. Here is flutter create's counter app, both ways.

Dart — Flutter
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('You have pushed the button…'),
            Text('$_counter'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: Icon(Icons.add),
      ),
    );
  }
}
Swift — Starling SDK
class _MyHomePageState: State<StatefulWidget> {
  private var _counter = 0

  private func _incrementCounter() {
    setState {
      _counter += 1
    }
  }

  override func build(_ context: any BuildContext) -> Widget {
    let homePage = widget as! MyHomePage
    return MaterialScaffold(
      appBar: MaterialAppBar(title: homePage.title),
      body: Center(
        child: Column(
          mainAxisAlignment: .center,
          children: [
            Text("You have pushed the button…"),
            Text("\(_counter)"),
          ]
        )
      ),
      floatingActionButton: MaterialFloatingActionButton(
        onPressed: { [weak self] in self?._incrementCounter() },
        child: Icon(CupertinoIcons.add)
      )
    )
  }
}

Same structure, same lifecycle, same setState. The differences are Swift's, not ours: override func instead of @override, a capture list on the closure, and enum cases that shorten to .center because the type is already known.

There is one addition. Because Swift has result builders, every container also takes a trailing closure, so if, for and switch work directly inside a widget tree — no spread operators, no .map().toList(). Both spellings compile to the identical tree, and a lint keeps them from drifting apart.

The fair question

“So why not just write Dart?”

If Dart suits your program, write Dart. Flutter is excellent and it is not in competition with this — the framework here is Flutter's, and we would rather it were understood as Flutter reaching a new language than as a rival.

This exists for the programs where the language is the constraint: where you need real shared-memory threads, or you live on top of native libraries, or you cannot have a VM and a garbage collector in the process, or your team simply writes Swift and has been waiting for a way to build a GUI with it that does not stop at Apple's platforms. That set turns out to be larger than it sounds — it is where systems software and applications overlap.

Our own proof is the extreme case: an entire desktop environment written on this SDK — compositor, window manager, X11 server, portals, and the apps on top of them. If the framework can carry that, an ordinary application is not going to trouble it.

Honest status

Where it runs today

We would rather be believed than impressive, so this is the real state rather than the roadmap.

Linux tested
The tested platform, in two shapes: as an ordinary desktop app on any session you already run (GNOME on Wayland, KDE, X11) through the engine's GTK embedder, and on bare DRM/KMS under the Starling desktop. The bundle tooling builds per-architecture trees; the versioned static-artifact route publishes x86_64 today.
macOS next
The package manifest has a macOS branch and a FlutterMacOSBridge target against the engine's macOS embedder, but nothing verifies them yet — that needs a machine and CI, and until it has both we will not claim it works. It is the next platform we intend to finish.
Windows, mobile reachable
Not started, and listed only because keeping the engine is what makes them tractable: the embedders already exist, so the work is bridging rather than porting a renderer. No timeline.

Known gaps

The framework does not yet answer System.requestAppExit, so a host closes its window directly rather than letting the framework veto. The flutter/keyevent channel replies are not yet the JSON the GTK keyboard handler expects, which logs a warning on focus changes. And on Ubuntu 26.04 a toolchain-versus-distro mismatch means consumers need two extra compiler flags — the workaround is in the repo, and it disappears when swift.org ships a native 26.04 toolchain.

Real code

Everything here was written with it

The most useful documentation for a framework is a program that uses it seriously, and this one comes with a dozen. The Starling desktop's first-party apps are ordinary SDK consumers — each a SwiftPM package that depends on the framework exactly the way your app will, with nothing privileged about them:

  • Calculator — ~480 lines. The one to read first.
  • Text Editor and Files — text editing, list virtualisation, real file I/O.
  • Terminal — a pty, an escape-sequence parser, and a custom-painted grid.
  • Settings — ~2,300 lines, nine panes, the largest of them.
  • The SDK's own examples — the Flutter counter, a todo app, and a port of the kalender package with day, week and month views.
And then the same thing, one level up: the desktop itself — compositor, window manager, dock, Mission Control, some twenty thousand lines of it — is a Swift program built on this framework. Its widgets are your widgets.

That is the part worth taking seriously if you are weighing whether the framework is real. There is no separate privileged API that the desktop uses and applications do not. The shell composites its windows into a widget tree, lays them out with the same constraint model as a Column, and rebuilds through setState. When we hit a limit, we fix it in the framework — which is why the framework is the part that gets better.

Build something with it

One download, four files, one swift build — no engine checkout, no toolchain beyond Swift. The guide walks the whole loop on an ordinary Linux session.