DEV Community

Kouki Badr
Kouki Badr

Posted on

I built a remote skin engine for Flutter

Every one of us has been here.

The designer opens Figma and changes the primary color from #6C63FF to #5B52E8. A small update (they think).

Our release cycle: create a branch, update the color, build, test, submit to App Store, wait for store review, for just a hex code.

This is the tax Flutter developers pay for a compile-time theming system. It's powerful and type-safe and beautiful — and completely frozen the moment your app ships.

flutter_skin

flutter_skin is a runtime skin engine. Instead of hardcoding your color values, you define them as named tokens. Those tokens are managed from a dashboard and delivered to your app.
When you publish a new skin, every connected device updates instantly.

How it works in 2 lines

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterSkin.init(apiKey: 'fsk_your_key_here');
  runApp(const MyApp());
}
Enter fullscreen mode Exit fullscreen mode
// In your MaterialApp:
theme: FlutterSkin.toThemeData()
Enter fullscreen mode Exit fullscreen mode

That's the integration. FlutterSkin.init() fetches the active skin from the FSkin backend and opens a persistent SSE connection. From that point, any skin you publish reaches the app in about 2 seconds.

The live update architecture

Here's what happens end to end when you click Publish:

Dashboard: click Publish
        ↓
Skin marked active in PostgreSQL
        ↓
Supabase Realtime detects the UPDATE on skins table
        ↓
Node.js backend receives the Realtime event
        ↓
Backend writes SSE event to all connected Flutter clients
        ↓
flutter_skin package receives the flag
        ↓
Package re-fetches active skin from CDN
        ↓
A stream emits new SkinTokens
        ↓
setState() → MaterialApp rebuilds → app repaints
Enter fullscreen mode Exit fullscreen mode

Total time: ~1–2 seconds. No polling. No App Store. No user action.

The full MaterialApp setup

To react to live updates, wrap your app in a StatefulWidget:

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    FlutterSkin.onSkinChanged.listen((_) {
      if (mounted) setState(() {});
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: FlutterSkin.toThemeData(),
      home: const HomePage(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

What's available today

The alpha supports color tokens — full Material ColorScheme mapping:

  • primary, secondary, background, surface, error
  • All the on* counterparts
  • brightness for light/dark

The dashboard (app.fskin.dev) gives you:

  • Project management
  • Skin editor with JSON view
  • Team collaboration with role-based permissions
  • API key management (auto-generated per project)
  • Publishing with confirmation + version history

A lot of cool features still coming

Links

It's still on alpha track.
I'd love feedback from anyone who tries it — bug reports, feature requests.
don't forget to thumbs up our package on pub.dev 🥸 👀.

Top comments (2)

Collapse
 
saqrelfirgany profile image
Ahmed ElFirgany

Cool project — SSE instead of polling makes sense here. I work a lot with offline-first apps, so my first question is the offline case: if the app is offline when a new skin drops, do you cache the last one, and how do you handle a stale cached skin meeting a newer one on reconnect? That's usually the tricky part.

Collapse
 
koukibadr profile image
Kouki Badr

Hello Ahmed, Indeed for offline apps there will be an offline handling by caching the last theme
for now with the current alpha version when the app goes offline it will show's your app default theme or fallback theme (I added the fallbackTheme attribute)
-> with the upcoming updates apps will handle cached versions of skins
for the conflict resolving between old stale skins and newer ones, we're implementing versionning already + currently the project already pushes the last skin as the one all apps should use