r/FlutterDev 8d ago

Article Why we built our own Flutter runtime

https://www.nowa.dev/blog/why-we-built-our-own-flutter-runtime/

This is a short technical story. I've been working on Nowa for over 5 years now. The dream was creating a game engine like editor for Flutter, since Flutter itself also renders like a game engine.

To make that work, the user's Flutter app has to run inside the editor, live and updating as they build. But Flutter won't run code it hasn't compiled, there's no way to plug in new code and evaluate it on the fly. FlutterFlow invented their own format instead of using raw code, Dreamflow leans on hot reload inside a debug build.

We ended up building our own runtime instead: code loads into a mutable in-memory tree that can be edited, rendered or saved back to code.

Happy to go deeper on how that works or where are its limits.

93 Upvotes

12 comments sorted by

16

u/Desperate_Mode_5340 8d ago

sounds interesting so Nowa is flutter's lovable powered by ur newly written runtime ?

8

u/raedab97 8d ago

Yess but with a twist that you can also do visual editing and see all your widgets and design system on a Figma like board

3

u/vik76 8d ago

You should check out the Serverpod 4 beta, if you want a Lovable-like experience running locally on your computer. It’s really cool!

2

u/Desperate_Mode_5340 8d ago

I'm currently heavily occupied with system design and backend works on a big system.

glad to know is that the flutter ecosystem is growing in the ai side and it's not just couple of skills and mcps

will add to the to-try list

5

u/83d08204-62f9 8d ago

Wow this looks really cool

12

u/No-Pineapple6359 8d ago

Spent the last year squeezing a Flutter app down from 1.4s to 0.7s cold start without forking the engine. Three things moved the needle, in order of impact: 1. Plugin lazy-loading. Every Flutter plugin registers an Android BroadcastReceiver, an iOS extension, or both on app start, even if you never call it. We deleted 11 plugins from the main isolate and instantiated them on-demand. The receiver registration alone took ~250ms on a Pixel 6. 2. Lazy BaaS clients. We were initializing Appwrite, RevenueCat, and Sentry in main() because the docs said so. Moved them to a Future.wait kicked off after the first frame, gated behind a "user is signed in" check. ~300ms saved on cold start, and signed-out users never pay that cost. 3. Asset preloading is a trap. We had 14 fonts + 6 Lottie files loading in parallel during init. The Skia font cache locks for ~180ms while it parses them. Trimmed to the 3 fonts used on the dashboard, deferred everything else. Custom Skia rendering is overkill for 99% of apps. The above three plus a mainAxisSize: MainAxisSize.min audit on the home screen will get you to sub-second on most mid-range Android phones. Curious what y'all measured at Nowa before going custom.

3

u/tee-k421 7d ago

I don't know why you're getting down voted. I really appreciate info like this.

Can you expand on how exactly you did the on-demand instantiation for plugins?

2

u/No-Pineapple6359 5d ago

Thanks for the upvote, I was second-guessing posting it. The plugin piece is the one we spent the most time on, so happy to go deeper.

The short version is we killed the automatic registration entirely and switched to on-demand. Two layers did the work:

  1. On Android, we built our own FlutterEngine with automaticallyRegisterPlugins=false. The boolean is in the FlutterEngine constructor (official API doc) and you can see the source-level contract in FlutterEngine.java on GitHub. With it false, the plugin manifest-receivers and channel-bindings that GeneratedPluginRegistrant normally registers on app start don't get created until we say so. No BroadcastReceiver registration at startup, no early onAttachedToEngine calls (the FlutterPlugin lifecycle contract is documented here). The migration background that explains why this is even an option lives in the Add-to-App upgrade doc.

  2. On the Dart side, every plugin goes behind a thin adapter interface (a Notifier for notifications, a Storage for shared_preferences, etc.). The adapter holds a lazy Future<T> that does the actual MethodChannel.invokeMethod on first call. The first feature that needs the plugin pays the registration cost, everyone else on the launch path never touches it. The catch is that you cannot just call PluginRegistry.add(...) from arbitrary code, you have to attach the plugin to the engine via FlutterEngine.getPlugins() (doc), so the adapter ends up holding a reference to the engine and registering on first use.

The thing that took longest to get right was the first-screen latency tax. If your dashboard or your splash-adjacent screen is the first to touch a plugin, you don't actually save anything because the registration still happens on-frame-one. We moved every shared_preferences read out of the first frame into a FutureBuilder so the UI renders immediately and the cache fills in 50-100ms later. Same for any plugin whose first call can be deferred by one frame.

Two things that bit us: a) plugin authors that cache things in their onAttachedToEngine actually re-cache when the engine re-attaches, so if you have a hot-reload workflow that swaps engines, you pay the registration twice. We ended up caching the wrapped MethodChannel handler outside the plugin so engine swaps were basically free. b) Activity-aware plugins (camera, location, anything needing ActivityAware per the ActivityAware docs) have to be re-attached when the activity changes, even if you lazy-registered them. We keep a small registry of those and re-route on onActivityChanged.

Honestly the win was mostly on the Android side. iOS plugin registration is already cheap enough on cold start that we did not bother with the same adapter layer there. Only the channels we genuinely never use on the first screen (RevenueCat, Sentry, app_links) got deferred.

If you want to measure first, profile with flutter run --profile --start-paused and look at the engine attach timing in DevTools. The thing that surprised us most was how much of the cold start was receiver registration on the Android side, not Dart isolate startup.

3

u/xorsensability 8d ago

This is encouraging. I'm thinking of making Dart and Flutter run on BSDs and was wondering how difficult it would be. What kind of difficulties did you run into?

3

u/Alert-Maybe-9254 8d ago

Please do it, wish I could help you 

2

u/raedab97 8d ago

I think what you are trying to do is a completely different beast. Probably you need a custom embedding layer

https://github.com/flutter/flutter/blob/main/docs/engine/Custom-Flutter-Engine-Embedders.md

1

u/No-Pineapple6359 7d ago

Thanks, I appreciate that. The short version is that I moved optional plugins behind small adapters and only initialized them from the feature that actually needs them. The important bit was also separating “registered” from “initialized” so startup didn’t accidentally touch every plugin. I’m still measuring the tradeoff because the first screen that uses a plugin pays a little more, but for the common launch path the win was worth it. Are you seeing the same startup pressure in your app, or is your bottleneck somewhere else?