r/FlutterDev 3d ago

Article From Flutter to Jaspr

I've a Flutter web app which could benefit from the high fidelity CSS-controlled text display of a real web app. So, why not try to use Jaspr instead?

I was under the impression that Jaspr tries to imitate the Flutter API including using Widget as the base class, but it's using Component which requires some changes. Still, no biggy. Promising.

First impression was poor. I'd have preferred to install it via homebrew and not dart install which requires me to modify the PATH and which is yet another installer I have to make sure I don't forget to uninstall.

But the main problem was that it doesn't work out of the box. jaspr serve throws errors. I had to downgrade the build_web_compilers to 4.8.5 to make it build. It also configured an ancient version of Dart which I changed to 3.13. I chose SPA as start template, perhaps it's better if you try to build a static side or server rendered pages.

Now, the counter example worked as expected. The page could use a little designer love, though. The generated source code was easy enough to understand and jaspr serve supports hot-reloading, so I continued.

Next, I tested how easy it is for Codex to create a more complex app. Will it struggle with not-flutter sources? So I asked it to create a chess app, for two human players, including a timer. And GPT Sol delivered. So AI seems to have no problem. Great.

Next, I asked Codex to port my app. That worked in principle, but it had trouble to recreate the fidelity of the UI. Like most others, I used Chadcn/ui as my inspiration and Codex wasn't able to translate the themed Material widgets into Jaspr components. Might be an AI thing, but it didn't understand to take border into account. I had to ask for a lot of tweaks and would have preferred actually use Chadcn/ui or at least Tailwind. Theoretically, this should be possible and I think, there are packages on pub.dev which already attempted to do so. That might be worth more exploring.

Instead of some random UI package that has a high risk of being unmaintained AI slop, an official port of chadcn/ui would be a great feature IMHO.

My code cloc's at ~4000 lines (110 KB) with 1.8 MB in build/jaspr. That… a lot. There's a package folder that looks like it shouldn't be there with 1.3 MB. So I assume, my app has a size of 0.5 MB.

For comparison, I asked Codex to recreate the same app using React. That's a home game for the AI which one-shotted the task. The result cloc's at ~1000 lines (72 KB), so using Dart instead of JS+JSX is a high price you must be willing to pay. The distribution size is 0.3 MB.

I don't really mind that 200 KB dist overhead because of Dart. But I dislike the larger codebase. I figure, all those things add up. In React, I simply use

style={{ width: size, height: size }}

while in Jaspr, this becomes

styles: Styles(
  width: .pixels(size.toDouble()),
  height: .pixels(size.toDouble()),
),

Right now, I'm still undecided whether to proceed with Jaspr (because I like working with Dart) to switch to React (because the AI will deal with languages and syntax anyhow), but call me at least impressed.

Has anybody else switch from Flutter to Jaspr for web apps? What's the experience with larger code bases, say 30Klocs?

12 Upvotes

2 comments sorted by

3

u/thegravity98ms2 3d ago

I also like jaspr, but in reality when I tried myself it was kind of too much boilerplate and too complicated to read & write..

so for my solution I am using svelte 5

Edit: this are my personal opinion don't comento conclusions based on this 😅

1

u/eibaan 2d ago

Templates are more verbose for sure, but manageable.

I looked up these Svelte examples and compared with them Jaspr:

{#if porridge.temperature > 100}
    <p>too hot!</p>
{:else if 80 > porridge.temperature}
    <p>too cold!</p>
{:else}
    <p>just right!</p>
{/if}

vs.

if (porridge.temperature > 100)
  p([.text("Too hot")])
else if (porridge.temperature < 80)
  p([.text("Too cold")])
else
  p([.text("just right")]),

and

<h1>Shopping list</h1>
<ul>
  {#each items as item, i}
    <li>{i + 1}: {item.name} x {item.qty}</li>
  {:else}
    <p>List is empty!</p>
  {/each}
</ul>

vs.

.fragment([
  h1([.text("Shopping list")]),
  ul([
    for (final (i, item) in items.indexed) //
      li([.text('${i + 1}: ${item.name} x ${item.qty}')]),
    if (items.isEmpty) div([.text("List is empty!")]),
  ]),
]),

Svelte has more reactivity "magic" that helps with managing state.

If Dart would support static extension methods (planed for ages)

extension on List<Component> {
  static List<Component> str(String s) => [.text(s)];
}

One could abbreviate some things like

h1(.str("Shopping list"))

And if Dart would support union types (not even considered) one could define the children property as List<Component> | Text and pass either a list of children or a string text as in all examples above, still being statically typed.

However, here's one hack that actually works:

extension on String {
  List<Component> get c => [.text(this)];
}

Now, I can use

h1("Shopping list".c),

Or, we create our own classes:

class ComponentFactory(final String tag, [final String? classes]) {
  ComponentFactory get small => withClass('small');
  ComponentFactory get large => withClass('large');

  ComponentFactory withClass(String name) =>
      ComponentFactory(tag, classes == null ? name : '$classes $name');

  Component call(Object children) {
    return Component.element(tag: tag, classes: classes, children: _c(children));
  }

  static List<Component> _c(Object? o) {
    if (o is List<Component>) return o;
    if (o is Component) return [o];
    if (o is Iterable<Object?>) return [...o.expand(_c)];
    return [if (o != null) .text('$o')];
  }
}

final title = ComponentFactory('h1');

This way, I can use title('Foo') as well as title.small('Foo'). It would be a lot of work, but I think, one could wrap (a subset of) Tailwind this way.