Start a project

Blog

Moving a live app onto a shared design system without changing a pixel

How we pulled Pocket Music's theme and generic widgets into one Flutter package for all our apps, and the screenshot diff that caught what 3,427 tests missed.

· 3 min read

We run three apps: Pocket Music, Sideman and Shedlog. They share a look on purpose — the same dark surfaces, the same orange action colour, the same 48-pixel touch targets — but until this month each app carried its own copy of the theme and of every button, dialog and list row. Three copies drift. A fix to a dialog in one app quietly stayed out of the other two.

So we moved the shared parts into one Flutter package, and moved Pocket Music onto it first. The rule for the move was strict: a user should not be able to tell that anything happened.

What went into the package, and what stayed out

The package holds the design tokens (colour, type, spacing, radius, motion), the theme built from them, the icon font, and the generic controls: buttons, dialogs, sheets, text fields, list rows, steppers, tab headers, toasts.

Deciding what stays out mattered more. We used one test: read the widget's imports. If it reaches into the app — its state management, its dependency container, its translations, its in-app purchase — it stays in the app. So does anything that only makes sense for music: the piano keyboard, the transport bar, the tool dock. A design system for a studio that builds apps for other people cannot be a music toolkit.

Generic widgets that used to read their text from the app's translations now take it as parameters. That is a little more typing at every call site, and it is what lets the same dialog work in an app that is not ours.

Proving nothing changed

Pocket Music already had 3,427 tests, and they all passed after the move. That told us the logic was intact. It did not tell us the screens looked the same, because most widget tests check what a screen does, not what it looks like.

So before touching anything we rendered every screen of the app to an image, in each variant, from inside the test suite. After the move we rendered them again and compared the two sets pixel by pixel. The target was simple: 71 out of 71 identical.

The first comparison was not 71 out of 71. It found two things no test had noticed:

  • A missing chip. Three tab headers in the app show a small Pro button in the corner. The package's tab header had no place for an app-specific widget, so the button had silently gone. The fix was a trailing slot on the generic header, which the app fills with its own Pro button. The package still knows nothing about Pro.
  • Text one pixel off. The token generator wrote line heights as rounded decimals. Flutter multiplies that ratio by the font size, so a rounded ratio lands a fraction of a pixel away, and some lines settled one pixel lower. The generator now writes the exact fraction — line height over font size — so the result is the line height the design asked for.

After both fixes: 71 of 71 screens identical, every test passing, and an app on the phone that looks exactly like the one in the store.

What we would tell anyone doing the same

  • Move one app first. The first move finds the gaps in the package. Ours was a missing slot and a rounding error; a second app would have found them twice.
  • Screenshots before refactors. Render every screen before you start. The baseline is cheap, and it is the only check that sees a widget that is gone rather than broken.
  • Generate the tokens, do not copy them. One file of tokens, one generator, the same numbers in the design files and in the code. When they disagree, the generator is where you fix it.
  • Stay in alpha until an app ships on it. The package is versioned 1.0.0-alpha and stays there until a release in the stores is built on it. Before that, breaking it is cheap and allowed.

Sideman and Shedlog move next. For a client project this is the same work done earlier: the first version starts on the shared system, so the next screen looks like the last one without anyone copying a file.

Have a first version to light?

Tell us what it should do. We reply within two working days.

hello@flamedots.com