The learning path

Eleven steps from never having used Gophics to shipping a real app. Each one ends in something you can point at — a running program, a passing test, a screen that survives a resize — rather than a topic you have read about.

Before you start

This path assumes things, and it is cheaper to say so than to let you discover it at step 4.

This is a young project. It is at v0.1.0 with no compatibility promise, no third-party package ecosystem, and a short list of real gaps — see the walls at the end. The path below is honest about where each one sits, because finding out at step 9 that the thing you planned your app around is unimplemented is a bad afternoon.

The path

  1. Put a window on the screen

    Clone the repo and run go run ./examples/hello. Then go run ./examples/todo and go run ./examples/hn. Nothing to write yet; the point is to confirm that go run really is the entire build.

    Outcome: three windows, opened with no toolchain step you had to install, and a reference set of working apps you will come back to all the way through this list.

  2. Make something respond to a tap

    Follow Get started and type out the counter yourself rather than copying it. Then break it deliberately: mutate s.n++ outside SetState and watch the number not change.

    Outcome: a stateful widget of your own, and a first-hand answer to why SetState exists rather than a remembered rule.

  3. Learn the two-tree model well enough to predict rebuilds

    Read Architecture, then the first half of the API tour. The one idea to come away with: a widget is an immutable value you throw away every frame, and an element is the retained node behind it that holds your state. Run any example with app.Config{Debug: true} to see the box bounds.

    Outcome: you can look at a tree and say which parts rebuild when one field changes — and you stop trying to mutate widgets.

  4. Write a test for what you just built

    This step is deliberately this early. Mount your counter with apptest.New, tap it by label, assert the label changed, and commit a golden image. Copy examples/counter/counter_test.go — including its apptest.Tol(apptest.AntiAliased), for the reason that file explains.

    Outcome: your UI is under test with no window, no emulator and no device farm. Forming the habit now costs an hour; retrofitting it onto a finished app costs a week.

  5. Lay out a screen that survives a resize

    Build a screen with a header, a body and a footer using Column, Row, Expand, Padding, Align and Sized. Then resize the window small, and resize it very wide. Read go doc ./layout Constraints once the layout misbehaves — constraints down, sizes up is the whole protocol, and it explains every surprise you will have had.

    Outcome: a layout that does not break at either extreme, and the vocabulary (CrossStretch, MainSpaceBetween, flex weights) to describe what you wanted.

  6. Put real data in a scrolling list, and navigate off it

    Use the scrolling-list recipe for the list, then add a widget.Navigator and push a detail page from a row — examples/hn is the worked version of exactly this. Key your rows with WithKey and then insert an item at the top to see why.

    Outcome: a two-screen app with a list long enough that only the visible rows are built, and state that stays with the right row when the list changes.

  7. Delete every colour literal

    Work through the theming recipe: derive a theme from theme.Light()/theme.Dark(), provide it above your tree, and read it with theme.Of(ctx). Then flip your OS to dark mode with the app running.

    Outcome: no paint.RGB call anywhere in your widget tree, and an app that follows the system colour scheme without you handling it.

  8. Reach the platform once, and handle nil

    Pick one capability your app would actually use — ctx.Preferences(), ctx.FilePicker(), ctx.Share() — and wire it with the capability recipe. Then run the same binary somewhere that does not have it and confirm the affordance disappears instead of failing.

    Outcome: one platform service in your app, and the habit that nil is an answer about the platform rather than an error to swallow.

  9. Draw the thing widgets cannot

    Every real app has one: a chart, a gauge, a board, a sparkline. Use widget.Canvas and the custom-painter recipe, and position your labels from MeasureWidthIn rather than from a guessed glyph width. If what you need is a conventional chart, look at the chart package before drawing it yourself.

    Outcome: a custom-painted widget whose text is placed by measurement, so it is still correct at a different font size.

  10. Make it usable without sight, and prove it in CI

    Label your drawn controls with Interactive.Sem, mark decoration Hidden, size touch targets to theme.MinTouchTarget, and add a semantics test from the accessibility recipe. Accessibility is implemented on macOS, Windows, Linux, the web and Android, so this is a real check and not a gesture.

    Outcome: a test in CI that fails when a control loses its name — the defect that is otherwise found by a procurement review.

  11. Ship it to a second platform

    gophics dev -p web . and open it in a browser. Then GOOS=windows GOARCH=arm64 go build ./... from whatever machine you are on. Read the walls below before you promise anyone a mobile release.

    Outcome: the same source running on a second target, and a realistic idea of what the remaining distance to a distributable artifact is.

Where to look when you are stuck

In this order, because that is the order that works:

The walls, named in advance

Honest version, so you can plan around them rather than discover them. The first group will stop you; the second will cost you.

WallWhat happensWork around it by
Colour emojiBitmap and SVG glyphs are not emitted, so a colour emoji draws nothing — not a box, nothing. Any app with user-entered text will show invisible characters.Nothing, yet. Monochrome symbol fonts work; plan for emoji to be absent.
Fonts on the webSystem font fallback is a deliberate no-op in a WebAssembly page — there is no font directory to scan. Runes outside your registered faces fall back to the primary face and draw as boxes.Bundling the faces you need in Config.Fallbacks. A full CJK face is tens of megabytes, so this is a real budget decision.
Native platform viewsYou cannot embed a native subview — a map, a video player, an in-app browser — in the widget tree on native targets. It is unimplemented.Opening the platform browser via ctx.Links(), or redesigning around it. If your app's core is an embedded map, this framework is the wrong choice today.
Hot reload (any platform)There is none, and there cannot be: Go has no way to replace code in a running process. What gophics dev does instead is state-preserving hot restart — rebuild, relaunch, land back on the same screen with the same scroll offset, field contents and navigator stack. That works on desktop, web, the Android emulator and the iOS simulator.Accepting the loop time. Measured on an Apple Silicon Mac: 1.4 s desktop, ~4 s web, ~11 s on an Android emulator, ~20 s on a pre-booted iOS simulator. Flutter's reload is ~1 s everywhere, so a mobile-first app with a lot of visual iteration will feel this.
Desktop packagingThe CLI builds a binary and stops. No .app bundle, no disk image, no installer, no AppImage, no notarisation. Mobile scaffolding, by contrast, is thorough.Writing the packaging yourself. A single static binary is genuinely easy to distribute to a technical audience and not to anyone else.
API stabilityv0.1.0, no compatibility promise. The generated API manifest records names but not signatures, so a signature-only change can pass a green build.Pinning a version in go.mod and reading the diff before you upgrade.
No package ecosystemZero third-party packages, and no extension point for them: the capability generator reads this repository's own source, so a capability cannot be added from outside the tree.Writing it yourself, or patching the tree. Everything Go-side — HTTP, databases, parsing — is of course unaffected, which is most of an app.
Painting expressivenessTwo-stop linear gradients by axis only; no radial or sweep, no blend modes, no dash patterns, no arbitrary-path clipping, no custom shaders.Composing from what is there. Most of these are approximable; none is as expressive as Flutter's painting API.
Animation velocityNo velocity handoff: a gesture released mid-flight cannot pass its speed into the animation that continues it, so flick-to-dismiss does not carry momentum.Using duration-based transitions and accepting the loss of feel, for now.
Internationalisation29 locales with hand-written plural rules. No ordinal plurals, no relative time, no locale-aware currency patterns, no message extraction tooling.intl for the common cases and your own formatting for the rest.

What is not on that list is worth saying too: accessibility on all five targets, per-platform scroll physics derived from device measurement, headless testing, terminal rendering, and a nine-dependency supply chain are all done, and several of them are ahead of the incumbents.

Start at step 1 →  ·  The cookbook  ·  Coming from Flutter or Compose