Cross platform options: A very shallow exploration

You’re starting a new app, which cross platform mobile framework should you choose?
You’ve had Cordova apps, and they made you sad, you’ve heard “why not native?”, and decided that’s not viable (for your own reasons), so what to do?
Well I built a similar toy app in a few of the big frameworks to compare them extremely subjectively (with some objectivity sprinkled in)
Honestly, I still don’t know which of them I like best.

A cross platform app is common for many clients, as they want users to be able to do their thing on a laptop, iPhone, Galaxy, fridge, etc.
As devs, we tend to want one codebase to rule them all, instead of one for web, one for iOS, and one for Android.

Which inevitably leads us…here.

The matrix was written in Javascript all along!
The matrix was written in Javascript all along!

The contenders I’ve decided to run with are:

I know there’s more, but these are my choices.

Contents

Setup #

I’ll be creating a silly app that uses some basic app elements like popups, inputs, database/other persistence, etc.

Flutter #

I’m going to be mixing Flutter in with Dart as I’m not 100% where the boundaries are.
Dart is an interesting language, and reminds me of SwiftUI (even though Dart was first)
Flutter + Dart does things like Futures, strong typing with late to escape, await/async, etc, and is built up with Widgets.
Widgets are composed together such that components are wrapped rather than adding attributes to them, i.e. to add padding, you wrap your widget in a <Padding> widget.

AV1 support in Edge requires a Microsoft Store Extension.

Things I like #

Things I don’t like #

I’m sure all of this has a reason, and I should just learn, but it feels like an unusual obstacle.
You learn a pattern, like const, but it doesn’t FEEL consistent.

Here’s an example of the build method a widget:

Expand/Collapse dart

Widget build(BuildContext context) {
  final myState = context.watch<MyAppState>();

  return Center(
      child: Column(children: [
    const Icon(Icons.airplane_ticket, color: Colors.purple, size: 50),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
        SizedBox(
          width: 100,
          child: TextField(
              onChanged: (value) => setState(() {
                    newCoolDude = value;
                  }),
              decoration: const InputDecoration(
                  border: UnderlineInputBorder(), labelText: "Your name")),
        ),
        Column(children: [
          const Text("Cool?"),
          Checkbox(
              value: isCoolDude,
              onChanged: (value) => setState(() {
                    isCoolDude = value ?? false;
                  }))
        ])
      ]),
    ),
    ElevatedButton(
        style: const ButtonStyle(
          backgroundColor: MaterialStatePropertyAll<Color>(Colors.green),
        ),
        onPressed: newCoolDude.isNotEmpty
            ? () {
                myState.addADude(CoolDude(
                    id: myState.getNextId(),
                    name: newCoolDude,
                    areTheyACoolDude: isCoolDude));
                setState(() {
                  newCoolDude = "";
                });
                Navigator.pop(outsideBuildContext);
              }
            : null,
        child: const Text("Add", style: TextStyle(color: Colors.white)))
  ]));
}
Widget build(BuildContext context) {
  final myState = context.watch<MyAppState>();

  return Center(
      child: Column(children: [
    const Icon(Icons.airplane_ticket, color: Colors.purple, size: 50),
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
        SizedBox(
          width: 100,
          child: TextField(
              onChanged: (value) => setState(() {
                    newCoolDude = value;
                  }),
              decoration: const InputDecoration(
                  border: UnderlineInputBorder(), labelText: "Your name")),
        ),
        Column(children: [
          const Text("Cool?"),
          Checkbox(
              value: isCoolDude,
              onChanged: (value) => setState(() {
                    isCoolDude = value ?? false;
                  }))
        ])
      ]),
    ),
    ElevatedButton(
        style: const ButtonStyle(
          backgroundColor: MaterialStatePropertyAll<Color>(Colors.green),
        ),
        onPressed: newCoolDude.isNotEmpty
            ? () {
                myState.addADude(CoolDude(
                    id: myState.getNextId(),
                    name: newCoolDude,
                    areTheyACoolDude: isCoolDude));
                setState(() {
                  newCoolDude = "";
                });
                Navigator.pop(outsideBuildContext);
              }
            : null,
        child: const Text("Add", style: TextStyle(color: Colors.white)))
  ]));
}

Padding wrapping sizing wrapping columns…
I find it very difficult to read and understand, but perhaps I just need to git gud.

Overall thoughts as a C# and web dev #

I don’t really like it, but that feels like my own bias/thought patterns, not that it’s actually a bad experience.
Composition especially just doesn’t click for me, however it seems easy enough to learn if required and well tooled, leading to a general recommendation.

You might notice that the app quality gets progressively worse as I get bored ☹️
I tried to maintain some quality, but time is not my friend.

MAUI #

Calling MAUI Xamarin Forms is perhaps a bit harsh, as it seems quite a bit of rework/fixing has gone into it, but it really is just Xamarin Forms++.
While that’s true, the experience feels better to me than XF did.
I’ve written about MAUI before and it was…fine. It’s fairly middle-of-the-road, decent but not amazing.

AV1 support in Edge requires a Microsoft Store Extension.

Things I like #

Things I don’t like #

See the code for my solution to this.

Overall thoughts as a C# and web dev #

It’s easy to pick up and run with, but does have the usual dotnet feeling where everything is achievable, but suffers from verbosity and multiple, similar, valid approaches.
A definite recommend, which is interesting considering Xamarin Forms would be an avoid.

MAUI Blazor Hybrid?? #

Blazor can be cross platform? Apparently yes!
Feels pretty nice too, considering it’s just Blazor with a MAUI shell.
That shell is very thin and I didn’t need to consider it at all with my silly app.

Perhaps using things like camera or biometrics would require digging into the MAUI side?

I’ve written about Blazor before as well, and I found it to be above average.

AV1 support in Edge requires a Microsoft Store Extension.

Things I like #

Ctrl + shift + i shows the debug tools. It was pain until I found this.

Things I don’t like #

Cordova, the fallen hero #

I didn’t even try Cordova for this, as I’ve had a growing number of issues in all my Cordova projects.
Flakey builds, critical but unmaintained plugins (push plugin, looking at you), confusing chains of plugins, confusing and inconsistent configuration.
I still have several Cordova projects, and I’m annoyed every time I have to touch it.
It was good while it lasted 😭

Svelte + Capacitor, the new Cordova #

Capacitor is the new Cordova and allows us to use familiar web frameworks and tooling and only have to worry about the cross platform part when we want to.
I decided to try out Svelte which has been on my radar for a while.

AV1 support in Edge requires a Microsoft Store Extension.

Svelte #

Svelte comes in two pieces, Svelte and SvelteKit, both of which are used here.
I’d describe it as HTML++, as it augments the existing HTML markup with some useful additions, like loops, variables, etc.
Its syntax and behaviours are familiar to a web dev and it’s additions to the HTML markup are clear and fairly simple to understand (mostly).
SvelteKit is the building side of the fence and has many adapters enabling various different approaches (nodejs, static pages, etc)

Capacitor #

Capacitor was reasonably simple to setup.
The instructions on the website are all I needed to get going.
SvelteKit has an adapter which can render to static pages, as long as there’s no server-side stuff, so I moved that to a quick backend in C#.
From there, it’s cap add android, cap sync, cap open android.
The only catch is adding the required plugins, which I found was fine here, but was a more significant piece of work when migrating existing projects.

Things I like #

Things I don’t like #

I had trouble getting Typescript working, but I think it was just the way I set it up.

Overall thoughts as a C# and web dev #

This is an excellent option, as you get it all with some limitations.
My silly toy project didn’t run into any issues, but I can imagine scenarios where using device features becomes a bit painful. That said, you have full access to the native projects as required.
The other really nice thing about this approach is the flexibility to use almost any web framework/libraries you want.
It’s especially great when you’re just wrapping a website for the appstores.

Recommendation for a new project #

If you’re a C# dev, MAUI or Blazor/MAUI is a decent choice.
If you’re a web dev OR wrapping an existing website, Web framework of choice + Capacitor is a good choice.
If you’re a fan of new shiny things, Flutter might be the one for you.

Other conclusions and thoughts #

This is hardly an exhaustive list, or even a deep investigation into the options presented.
However, I think there’s some learnings here despite the toy examples.
The time implementing each was about even, although it’s hard to measure considering I was doing significant learning every step of the way.
My original intent was to compare the picking up experiences of each approach, but I’m not sure I got a good measure of that.
My gut says they’re all on a pretty even level for onboarding.

There’s much more for me to learn on the mobile journey, however I know that I’m working with Capacitor and MAUI right now.
It’s likely most of my powers will go into those two for the time being.

Github links #

It’s all awful code.
I’m sorry, I started lazy and only got lazier.

Flutter
MAUI
MAUI + Blazor
Svelte