Flutter for SwiftUI Developers (and 🔄)

In the last few months Flutter gained some popularity as a cross platform UI framework. We’ll have a look at it from a SwiftUI developer’s perspective, as it has a quite similar way to construct user interfaces, “declaratively”.

This is going to be a longish article looking at various aspects. Feel free to jump around:

Disclaimer: Flutter knowledge is from playing with things for about a week. Feel free to send corrections to me@helgehess.eu.

Introduction

Having seen “XP kits” come and go for over 25 years (yes, including YellowBox for Windows, XUL or Swing), I generally believe that it is proven by history that using cross platform UI frameworks is almost universally the wrong™️ approach.
They tend to produce a look and feel that is off on all platforms, rarely integrate with platform features, are often slugish and also fail to deliver lower development costs.

But hey, it is still interesting and fun to play with things and see how stuff fits together 🤓 Flutter is quite interesting for SwiftUI developers because it is very similar. Almost feels like Apple hired the original Flutter developers to produce a “better” version.

So I’ve been interested what the hype is about and took some time to build a small Flutter app, to learn how that goes and how it compares to SwiftUI.

Oh, something to keep in mind: Flutter is a Google project.

As a preview, this is what a simple Flutter app looks like, we’ll get to all the details later:

import 'package:flutter/material.dart'; // import module

void main() {            // the Dart app entry point
  runApp(const MyApp()); // const is funny, later!
}

class MyApp extends StatelessWidget { // Application
  const MyApp({Key? key}) : super(key: key); // named parameter

  @override // var body: some View
  Widget build(BuildContext context) { // ctx is the Environment
    return MaterialApp( // use the ugly Material styling
      title: 'Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(title: 'Demo')
    ); // notice all the `;`? required!
  }
}

class MyHomePage extends StatefulWidget { // a View
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title; // non @State View ivar, final is let

  @override  // A `StatefulWidget`, State is extra
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> { // generics
  int _counter = 0; // roughly an ObservableObject ivar

  @override // var view: some View
  Widget build(BuildContext context) {
    return Scaffold( // ~NavigationView
      appBar: AppBar(title: Text(widget.title)), // .navigationTitle...
      body: Center(child: Column( // ~ZStack(VStack(...))
        mainAxisAlignment: MainAxisAlignment.center, // VStack(alignment:)
        children: [
          const Text('Count:'),
          Text('$_counter') // String interpolation
        ]
      )),
      floatingActionButton: FloatingActionButton(
        onPressed: () { // closure
          setState(() { // objectWillChange.send()
            _counter++; // ++!!
          });
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add)
      )
    );
  }
}

It is more boilerplate and syntax, but pretty close to what one would do in SwiftUI.

Setup

Installing Flutter using Homebrew is simple (and doesn’t seem to clutter the system much or do unexpected things):

$ brew install flutter

And since we are going to use Visual Studio Code as the “IDE”, this might be useful:

$ brew install --cask visual-studio-code

I had a look at VSCode for Swift in this article, if an introduction is desirable. There doesn’t seem to be a nice looking Mac IDE for Flutter, maybe Nova? VSCode is ugly but bearable.

There is a flutter doctor command that can be run to check whether the Flutter installation went well:

$ flutter doctor
[✓] Flutter (Channel stable, 3.0.1, on macOS 12.4 21F79 darwin-arm, locale en-DE)
[✗] Android toolchain - develop for Android devices
...

To create a new project, flutter create is used:

$ flutter create cowtastic
Signing iOS app for device deployment using developer identity: "Apple Development: Helge Heß (ABC12DEFGH)"
Creating project cowtastic...
Running "flutter pub get" in cowtastic...                          918ms
Wrote 127 files.

All done!
In order to run your application, type:

  $ cd cowtastic
  $ flutter run

Your application code is in cowtastic/lib/main.dart.

Yes, no kidding, this produces a folder with no less than 65 directories and 132 files. The sole interesting to us is main.dart, living in the lib subfolder.

This can be run on the command line already, but it is more useful to go straight to Visual Studio Code. There are two extensions which seem useful and should be installed (using the extensions tab on the left): Flutter and Pubspec Assist.

This is what it will look like:

When starting the app for the first time, macOS signing issues come up. In case Google (and Homebrew) are trusted, confirm such in the macOS preferences:

To select whether you want to test the app on a device, on a simulator or run it as a “Mac app”, click the target at the lower right. Then chose “Start iOS Simulator” for simulator (or stick to the device):

This will just start the simulator.

Pressing F5 builds and starts the app (and potentially brings up the macOS security panels until things are granted). Starting the app takes a moment as things are being xcodebuild’.

Congratulations, first Flutter app is running.

You’ll notice that the app looks quite wrong for an iOS app. That is because everything defaults to Google’s “Material” theme (and most of the tutorials rely on it). It can be changed to look a little more like UIKit, we’ll get to that.

Basic Concepts

It may look a lot like Java on first sight, but Flutter apps are actually written in an own programming language called Dart. It’s OK, less strict. Swift looks nicer, has way more conveniences. Dart compiles myriads faster - literally on save. And has a proper GC, no ARC. A few language examples follow below.

Flutter itself is a framework on top. The key concept of Flutter is a Widget, which is essentially the same thing like a SwiftUI View (though it is also used for ViewModifier’s, App, everything). Their tagline is:

Flutter, where Everything is a Widget™

I wonder whether the common desire to force everything into Views on SwiftUI is rooted in that.

What is different to SwiftUI is that a Widget itself doesn’t really have state, it is immutable. It can be a StatefulWidget, but those still don’t carry the state inside the Widget, but in a separate, State<Widget> object. Pretty similar to our ViewController.
Speaking of objects, everything in Dart is classes and objects (though often immutable or const), no structs.

Comparing the HelloWorld App

In the introduction I’ve shown the boilerplate app flutter create generates. It is a simple screen with a Text, showing a counter, and a button to increase that counter. Because it has to store the current count it involves a StatefulWidget.

Note how the SwiftUI app an the right looks proper by default and the Flutter app on the left looks like an Android thing. We will look at that later.

This is roughly what the same app would look like in SwiftUI:

import SwiftUI

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      MyHomePage(title: "SwiftUI Demo")
    }
  }
}

struct MyHomePage: View {
  
  let title : String
  @State private var counter = 0
  
  var body: some View {
    NavigationView {
      VStack(alignment: .center) {
        Text("You have pushed the button this many times:")
        Text("\(counter)")
          .font(.title)
      }
      .navigationTitle(title)
      .toolbar {
        Button(action: { counter += 1 }) {
          Label("Increment", systemImage: "plus.circle")
        }
      }
    }
  }
}

Let’s compare the different parts.

App Setup

In both environments there is a main entry point, where the application itself is configured. It is just a main function in Dart and Swift got the magic @main annotation in 2020.

import 'package:flutter/material.dart';

void main() { runApp(const MyApp()); }

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) => MaterialApp(
    title: 'Demo',
    theme: ThemeData(primarySwatch: Colors.blue),
    home: const MyHomePage(title: 'Demo')
  );
}

SwiftUI:

import SwiftUI

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      MyHomePage(title: "Flutter Demo Home Page")
    }
  }
}

Flutter has more boilerplate, it kinda feels like how you would write a SwiftUI-like framework in a language you don’t control (weird as it seems tightly coupled to Dart).
Swift got a set of specific language enhancements to support SwiftUI, including Property Wrappers and Result Builders. Those make SwiftUI sources look nicer, but not necessarily easier to follow along.

As mentioned, Flutter uses Widget for everything, while SwiftUI has more specific types, like App, but the idea is the same:

Widget build(BuildContext context) => MaterialApp(...);

In SwiftUI the build function is a computed body property:

var body: some Scene { ... }

In Flutter the build functions carry around a BuildContext, which is quite similar to the SwiftUI Environment. E.g. it is used to pass down FontStyle’s, themes and something comparable to @EnvironmentObject’s.

MyHomePage Top Level Widget

The page setup is a little more interesting (in SwiftUI the “root view” is commonly called ContentView, but I went with MyHomePage to match up w/ the Flutter setup).

State

In Flutter “state” is handled separately from Widgets, while in SwiftUI allows View “configuration” and its “state” to be intermingled in a single entity. The latter makes for nice compact demos (as this) but generally tends to lead to spaghetti code once apps start to grow. When doing things cleanly in SwiftUI they end up looking quite similar.

So in Flutter, the MyHomePage is a StatefulWidget:

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title; // final is let

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

Note that “StatefulWidget” doesn’t mean that the widget itself carries state, it is still the immutable “configuration” (the title parameter). Instead the state is pushed into a separate State object:

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) { ... }
}

Very similar to what people do in clean SwiftUI code and often call “ViewModels” (implemented as ObservableObject’s).
What I found a little surprising that the State itself also has a build function, instead of the associated widget having a build function that can get to the State. That is very similar to our ViewController approach. State is such a ViewController, it is a very specific ObservableObject that has a View attached, which is being controlled.

This is what the plain, demoware, version in SwiftUI looks like:

struct MyHomePage: View {
  
  let title : String
  @State private var counter = 0
  
  var body: some View { ... }
}

It carries the title configuration as well as the counter @State. This is way more compact then the Flutter version w/ two objects, but it also adds a lot of magic and often unexpected behaviour. @State essentially turns the View “struct” into an object (i.e. it gets an “identity”).
Here I actually prefer the Flutter setup where the State has its own identity, and as mentioned, that’s what you often end up with in SwiftUI anyways.

There is one more aspect to state management in that Widget/View, how the state is modified.

Flutter:

onPressed: () {
  setState(() { 
    _counter++;
  }
}

SwiftUI:

action: { counter += 1 }

This is much more compact in SwiftUI because the @State property wrapper sees the modification and automatically marks the View for re-evaluation (similar thing w/ @Published).
In Flutter the modifications have to be wrapped in a setState() call, which tells the system that things changed explicitly (presumably it does snapshotting and diffing as part of that).

Page Layout

The way the build/body is implemented is quite similar again. There are two significant differences:

  • SwiftUI uses Result Builders, essentially a different language syntax with some benefits to both source code beauty and performance (they preserve the full static type of the View tree).
    Flutter just uses regular function calls in the common builder pattern.
  • Flutter doesn’t usually use ViewModifier’s like .padding (though I think that would be possible, it is just not idiomatic), but rather wraps Widgets in Widgets in Widgets (e.g. in a Container for padding).
Widget build(BuildContext context) => Scaffold(
  appBar: AppBar(title: Text(widget.title)),
  body: Center(child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      const Text('Count:'),
      Text('$_counter')
    ]
  )),
  floatingActionButton: FloatingActionButton(
    onPressed: () => setState(() { _counter++; }),
    tooltip: 'Increment',
    child: const Icon(Icons.add)
  )
);

This is just calling into the constructors of the respective widgets, with the arguments being subwidgets, sometimes for specific sections. No trailing closures or result builders, hence the subviews live within the arguments of the widget’s “init call”.

Widgets used:

  • Scaffold is a widget from the “Material” library. It essentially provides the layout w/ the title bar, the floating button, and the page contents. Somewhat similar to NavigationView in SwiftUI.
  • AppBar is a widget drawing the actual title bar. Similar to the .navigationTitle modifier and companions.
  • Center just centers its single child Widget in the available space, kinda like a ZStack with just one child, though it also “expands” (takes the available space).
  • Column is a VStack, its mainAxisAlignment says how the content distributes vertically, somewhat related to .layoutPriority. The crossAxisAlignment is the same like the alignment parameter of the VStack (uses start instead of leading, end instead of trailing, note that the enum needs to be spelled out, just .center is insufficient).
  • Text, which is just like Text. Though the interpolation (Text('$_counter)) is done by the language, not by Text itself. I.e. it probably can’t do the localization things SwiftUI can do.
  • Other containers are:
    • Row, which is the HStack
    • Container, which is used to add padding and decoration like borders, background colors.

Approximately the same in SwiftUI:

var body: some View {
  NavigationView {
    VStack(alignment: .center) {
      Text("You have pushed the button this many times:")
      Text("\(counter)")
        .font(.title)
    }
    .navigationTitle(title)
    .toolbar {
      Button(action: { counter += 1 }) {
        Label("Increment", systemImage: "plus.circle")
      }
    }
  }
}

I think that SwiftUI definitely looks nicer, but the Flutter thing is quite reasonable as well. And doesn’t require any extra language features.

Intermission: Things that are great in Flutter.

That was a lot already, let’s have a video. Something that is really great about Flutter is the speed of iteration, seeing is believing:

The app was originally started using F5, which takes a moment. But you rarely ever do that. Once started you just edit a source file (here we change the theme color from blue to green), save the file and 💥 boom, the app just updates in the simulator.
And it just works. Like always. It is nothing like Xcode previews which are a trainwreck except for the most simple scenarios (I tend to create own Xcode projects just for previews, when having to iterate over some visuals of a View).

This part is really unbelievably good about Flutter development. Swift has a hard time here due to it being extremely static and incredibly slow. We’ll see whether WWDC 2022 brings improvements. I kinda doubt it.

Another thing that is just great in Flutter, but awful in SwiftUI, is the documentation. Don’t get us wrong, the SwiftUI documentation is extremely beautiful. What it doesn’t do well is document actual SwiftUI development (presumably because it is written by people who never wrote a real SwiftUI application).
And the documentation of symbols, usability is close to zarro 🙈

Flutter documentation is the opposite. A little ugly, but extremely helpful and always to the point. It feels like it is written by developers who actually used Flutter and know what information a developer is looking for. From Getting Started, over class documentation, to the Cookbook. Great stuff.

I think those two things are where Flutter doesn’t flutter but really flies. But let’s go back to the tech.

Dart and Swift

If you used a C or Java language before, it is pretty straight forward. What follows is a small overview of Dart syntax.

You have to use semicolons, coming from Swift I forgot them all the time:

onPressed: () {
  setState(() {
    _counter++; // <= semicolon!
  });           // <= another semicolon!
},

Also shows Dart closures, above two closures without parameters:

( parameters ) { code }

It does not have trailing closures like Swift, e.g. you cannot write it like this:

setState { // doesn't fly in Dart
  _counter++
}
setState(() { // needs to be inside the argument list
  _counter++;
});

There are a lot of those small syntactic things which make Swift look more beautiful and clean, though oftentimes it actually makes it harder to read.

You also have to use return in blocks that return a value, e.g. this returns a new Widget (read: View):

Widget build(BuildContext context) {
  return MaterialApp(...);
}

Though there is a similar shortcut syntax like in JavaScript, if the function is just a single expression. I found it to be applicable often, and generally nice looking:

Widget build(BuildContext context) => MaterialApp(
  ...
); // <= semicolon needed here!

Also shown: the return types of functions and arguments and variables are spelled before the name, not after. Like in C/Java. Also no func keyword is needed. E.g. the above would be this in Swift:

func build(_ context: BuildContext) -> Widget {
  MaterialApp(...)
}

Dart has both positional and named parameters, like Swift. Shown above are positional, i.e. build is called like this:

widget.build(context);

not like this (which would be the default in Swift, w/o the _ placeholder):

widget.build(context: context); // wrong for positional

Named parameter declarations have a syntax that is a little weird:

MyApp({Key? key})

key is the parameter name, Key? the type.

Which brings us to Optionals (Key? - an optional Key). Current Dart has the concept of nullability and nullability checks, but does it a little less “in your face”. Generally, like in Smalltalk, all values in Dart are objects (and allow a null variant). That even includes things like int:

final int  a = 42;   // pushing null raises error
final int? b = null;
if (b != null) return b!;
final int  c = b!;   // force unwrap is there, error throws
final d      = c;    // "auto" also available

The final is the same like let in Swift:

let a : Int  = 42;   // not possible to set to `nil`
let b : Int? = nil;
if let b { return b }
let c : Int  = b!;   // runtime crash
let d = c

Note that Dart requires parenthesis () in if conditions (and for etc), but allows single statement blocks w/o braces {} (if (x) return 42;).

Shown before, Dart doesn’t have have Result Builders like in this Swift:

VStack {
  Text("Hello")
  Text("World")
}

which is a clever but complicated thing explained elsewhere, instead things are constructed using regular calls:

Row(children: [
  Text("Hello"),
  Text("World")
])

Which isn’t that awful from a coding perspective.

I don’t want to go too much into generics, but generally in SwiftUI you’ll get to fight with them more often (for somewhat good reasons). I.e. things like AnyView are not necessary in Flutter, you just use the Widget base class.

Dart has stdlib functions like map, which return an Iterable. Again, less generic boilerplate that has to be dealt with. Sample:

_saved.map( (pair) => Text(pair.asPascalCase));

The Swift Any type is called dynamic in Flutter. E.g. this Swift:

let json : [ String : Any ]
let json : Dictionary<String, Any> // or this

looks like this in Dart:

final Map<String, dynamic> json;

To cast, Dart has is and as:

List<SPIPackage> _decodeJSON(dynamic json) {
  final jsonDict = json as Map<String, dynamic>;
  final results  = jsonDict["results"] as List<dynamic>;

  for (final result in results) {
    final json = result as Map<String, dynamic>;
    if (json["package"] == null) continue;

The way to work w/ a JSONSerialization like JSON result. If the type doesn’t match, they fail with a runtime error. “Deep casts” don’t work like in Swift. E.g. this doesn’t fly:

final typed = json as Map<String, Map<String, List<Map<String, dynamic>>>>

The casting has to be done step-by-step.

(Thanks god) there is no Coddable in Dart. Though it also disables runtime reflection, which results in the developer having to do it manually (which I think is OK):

factory SPIPackage.fromJson(Map<String, dynamic> json) => SPIPackage(
  packageId      : json['packageId'],
  repositoryName : json['repositoryName'],
  ...
);

… or resort to code generation (which I dislike, code generation is wrong™️). This also does an implicit as cast, to String in this case:

class SPIPackage {
  final String packageId;
  final String repositoryName;
  ...
}

final class doesn’t work, btw. Nor can you nest a class in another class.

There is async/await, but it is Future based. The compute function starts things in an isolate, which seems kinda like a Swift actor. An example:

class _SPMListPageState extends State<SPMListPage> { 
  late Future<List<SPIPackage>> _packages; // late is like lazy

  Future<List<SPIPackage>> _querySPI(String query) async { // <= async!
    final response = await http.get( // <= await
      Uri(scheme: "https",  host: "swiftpackageindex.com",
          path: "/api/search", 
          queryParameters : { "query": query })
    );

    if (response.statusCode == 200) {
      return _decodeJSON(jsonDecode(response.body));
    }
    else {
      throw Exception("Failed to run query! ${response.statusCode}");
    }
  }
  ...
}

How does that integrate into Flutter Widgets? Everything is a widget! FutureBuilder:

Widget _buildResultView(BuildContext context) =>
  FutureBuilder<List<SPIPackage>>(
    future: _packages,
    builder: ( ctx, snapshot ) {
      if (snapshot.hasData) {
        return _buildResultList(context, snapshot.data as List<SPIPackage>);
      }
      else if (snapshot.hasError) {...}
      else { PlatformCircularProgressIndicator()); }
    }
  );

The Swift typealias is also available and called typedef in Dart, from UXKit.dart:

typedef UXApp        = PlatformApp;
typedef UXScaffold   = PlatformScaffold;
typedef UXAppBar     = PlatformAppBar;
typedef UXIconButton = PlatformIconButton;

Another funny thing the attentive reader may have noticed are the _ prefixes everywhere, like:

late Future<List<SPIPackage>> _packages;
Widget _buildResultView(BuildContext context)

It is exactly what you’d think it is: the _ prefix marks things as internal/private. Symbols w/o are public. E.g. the State of a StatefulWidget is usually private and starts with an _.
I think that’s not the worst approach as it removes clutter and is obvious.

A few data types:

final answer         = 42;            // an int
final isIt           = "1337";        // a String
final setOfWordPairs = <WordPair>{};  // a Set
final wordPairs      = <WordPair>[];  // a List, Array in Swift
final answers = { 42: "The Answer" }; // a Map,  Dictionary in Swift

final answer = answers[42];

Strings have interpolation, but not the customizable subsystem Swift has:

final singleVar = Text('$answer');
final exprPath  = Text("${answers[42]}");

To finish the Dart overview, it has a funny const construct you see a lot:

Column(children: [
  const Text('Count:'),
  Text('$_counter')
]);

It is a little like a callsite singleton and makes sure that just one instance of a “constant object” exists. E.g. in this case the Text w/o a variable reference is a compile time constant and won’t be required each time. That “constantness” can propagate through hierarchies, like that:

const Column(children: [
  Text('Hello'),
  Text('World')
]);

VSCode tells you if a const should be added.

Packages

🐄 Time to add some cows! 🐮

Dart comes with a package system and has an official package registry: pub.dev. Swift has SPM and the (unofficial but great) Swift Package Index.

What Package.swift is to Swift, pubspec.yaml is for Dart:

name: cowtastic
description: A new Flutter project.
version: 1.0.0+1

environment:
  sdk: ">=2.17.1 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
flutter:
  uses-material-design: true

Should be self-explanatory.

Packages can be added manually, or if the Pubspec Assist VSCode extension was installed, using that (⌘⇧ P, then “Pubspec”, “Add/update deps”):

Dart doesn’t seem to have a great cows package like Swift (or JavaScript), but at least cowsay is available. This gets added to the pubspec.yaml:

dependencies:
  cowsay: ^1.0.0
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter

Generally having a declarative package format seems like a waaay better approach than what SPM does with Package.swift (and all the pain that comes due to that).

To use cowsay, it needs to be imported into the main.dart file. Package lookup has (working!) auto-complete in VSCode:

import 'package:flutter/material.dart';
import 'package:cowsay/cowsay.dart';

Note how a specific .dart file is imported, not really a package. And package is an URL scheme making the compiler lookup the file in a specific file structure.

Cowsay can then be used in the build function (aka body) of the State:

body: Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.start,
    children: [
      Text(Cowsay.getString('Hello!')), // <==
      const Text(
        'You have pushed the button this many times:',
      ),

And et voilà (again, just ⌘s to save, compile and live-deploy!):

This doesn’t use a monospaced font, and it actually took us quite a while to figure out how to get that. In SwiftUI it would be a simple Font.monospaced:

Text(cows.vaca())
  .font(.body.monospaced())

In Flutter I had to tie that to a specific font:

Text(cowsay.getString('Hello!'), 
     style: TextStyle(fontFamily: "Menlo"))

There is probably a better way.

Multiple Files in your Package

To use multiple files in Swift, you just create them next to each other in the same target folder. There is no need to explicitly import them, which is nice and clean. But presumably not exactly helpful for the compilation speed.

In Dart additional files can also be added in the lib directory, alongside the main.dart. But to use them, they need to be imported. E.g. if you moved the MyApp class to an own my_app.dart file (yes, snakecase is what they do in Dart for filenames):

import 'my_app.dart';
import 'uxkit.dart';

Platform Widgets

When I first played w/ Flutter, I’ve been kinda surprised that we get the Android Material look. Well, not exactly surprised about that. But that there is no simple switch to tell Flutter to render like an iOS app. And it looks like there really is none!

Instead Flutter comes with a set of “Material” widgets, which is what the boilerplate app imports:

import 'package:flutter/material.dart';

and a completely distinct “Cupertino” widget set:

import 'package:flutter/cupertino.dart';

Yes, those are different widgets, with different parameters and layouts. And Material, widgets like ListTile can’t be used as children of say a CupertinoPageScaffold.

To get the iOS look, a different page has to be written:

Widget build(BuildContext context) => CupertinoApp( // <==
  title: "Flutter Demo Home Page",
  home: const MyHomePage(title: 'Flutter Demo Home Page')
);
Widget build(BuildContext context) =>
  CupertinoPageScaffold( // <==
    navigationBar: 
      CupertinoNavigationBar(middle: Text(widget.title)),
    child: ListView(children: [
      Column(children: [
        Text(Cowsay.getString('Hello!'),
        style: const TextStyle(fontFamily: "Menlo")),
        const Text(
          'You have pushed the button this many times:',
        ),
        Text(
          '$_counter',
          style: Theme.of(context).textTheme.headline4,
        )
      ])
    ])
  );

A little crazy isn’t it? Well, maybe not 😎 It actually follows the learn-once-apply-anywhere mantra of SwiftUI. Different platforms need different configurations anyways to look proper.

Also remember that Flutter doesn’t actually use UIKit (like SwiftUI does for many things under the hood)!
Both, the Material and Cupertino sets do all the rendering within Flutter, i.e. they are reimplementations of the UI frameworks. Just due to that they have a different look and feel, get outdated when the main OS goes forward, and simply feel “off” on all platforms.

OK, so do we have to write separate Flutter apps for the different platforms? Not necessarily. There are a few packages abstracting the two widget sets into a common cross platform one.
E.g. Flutter Platform Widgets, which can be added as a dependency and then get imported:

import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';

This contains widgets that switch to either Material or Cupertino, depending on the platform:

Widget build(BuildContext context) => PlatformApp(
  title: "Flutter Demo Home Page",
  home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
Widget build(BuildContext context) =>
  PlatformScaffold(
    appBar: PlatformAppBar(title: Text(widget.title)),
    body: ListView(children: [
      Column(children: [
        Text(Cowsay.getString('Hello!'))
...        

It also has a PlatformWidget that allows for different widgets on each like so:

PlatformWidget(
  cupertino : (_, __) => Text(cow)
  material  : (_, __) => ListTile(title: cow)
)

I’m not really sure what to think about that. That this isn’t the default was a little surprising. In particular because the Cupertino widget set doesn’t actually use UIKit. Presumably everything started out as Material and then people actually tried to use Flutter on iOS.

Because those widget names can get long, I’ve created myself a small UXKit.dart:

import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';

typedef UXApp        = PlatformApp;
typedef UXScaffold   = PlatformScaffold;
typedef UXAppBar     = PlatformAppBar;
typedef UXIcons      = PlatformIcons;
typedef UXIconButton = PlatformIconButton;

(Note that just Scaffold and such often clashes with the Material set, as those have unqualified global names, so a prefix should be used.)

It is also worth noting that SwiftUI does seem to go the way of abstracting (Apple) platform specific things like navigationBarTitle (iOS specific concept) into a more general navigationTitle. So maybe we do get write-once in SwiftUI 🧐

Advanced State Management

State management is a funny topic, because it kinda matches what SwiftUI does here: Not much 🤓 Instead multiple approaches are supported: List of state management approaches (but documentation, again: :chefskiss: here!).

As we’ve seen Flutter itself decouples the State from the StatefulWidget, giving the setup more structure. Where the State object is kinda how SwiftUI people use ObservableObject “ViewModels” or ViewController’s.

Flutter also comes with a concept of a more general observable object, and @EnvironmentObject. The ObservableObject’s are called ChangeNotifier’s in Flutter and look like this:

class CartModel extends ChangeNotifier {

  final List<Item> _items = [];
  int get totalPrice => _items.length * 42; // computed property!

  void add(Item item) {
    _items.add(item);
    notifyListeners();
  }
}

In Swift that would look like:

class CartModel: ObservableObject {
  
  @Published private var items = [ Item ]()
  var totalPrice : Int { items.count * 42 } // yes,yes

  func add(_ item: Item) { items.append(item) }
}

The @Published property wrapper does the change notification magic when the array is modified (it will invoke objectWillChange.send()).

In Dart the notifyListeners method needs to be called explicitly, after the change happened. (Which is funny, because SwiftUI originally started out with didChange but switched to objectWillChange prior the first release.)

The ChangeNotifier is part of Dart/Flutter itself. But even though Flutter carries along the BuildContext, which is kinda like the SwiftUI environment, it doesn’t have an @EnvironmentObject itself. That can be added using the Provider package.

To create and inject an environment object, the ChangeNotifierProvider is used:

class Root extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    ChangeNotifierProvider(
      create: ( ctx ) => CartModel(),
      child: const HomePage()
    );
  }
}

This is kinda similar to:

struct Root: View {

  @StateObject var cart = CartModel() // autoclosure, also on-demand!

  var body: some View {
    HomePage()
      .environmentObject(cart)
  }
}

To access the model, the Consumer widget is used:

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) => Consumer<CartModel>(
    builder: ( ctx, cart, child ) {
      return Text("Total: ${cart.total}");
    }
  );
}

This will refresh the child widget if the model changes.

In SwiftUI the same would look like:

struct HomePage: View {
  
  @EnvironmentObject var cart : CartModel
  
  var body: some View {
    Text("Total: \(cart.total)")
  }
}

Again SwiftUI gets several extra points for looking nicely, but what is going on is arguably easier to understand in the Flutter setup.

BTW: Not 100% sure, but I think there is also a difference in that Flutter always rebuilds all child widgets on a change. It is complicated, but generally in SwiftUI child Views need to explicitly subscribe to the respective environment/observable objects to get refreshed (either using @EnvironmentObject or @ObservedObject).

Overall similar concepts in SwiftUI and Flutter. Also, both refer to other packages for more complex setups. I recommend ViewController 😬

2022-06-05: SwiftUI 4 (iOS 16+) has a new navigation API that fixes a lot of things. Learn about it over here.

Navigation in SwiftUI is pretty much a buggy mess. There are few topics which get as many complaints.

In SwiftUI navigation is bound to state, which kinda make sense for such a framework. A little example:

struct MainPage: View {

  @State var showDetail = false

  var body: some View {
    NavigationView {
      VStack {
        NavigationLink("Goto detail", isActive: $showDetail) {
          DetailPage()
        }
      }
    }
  }
}
struct DetailPage: View {
  ...
}

To trigger the navigation programmatically, you’d “just” change the state of the showDetail state:

func gotoDetail() {
  showDetail = true
}

And it’ll happen magically. Until it doesn’t anymore. It really is a dispatchy-main-asyncAfter mess.

In Flutter the app’s navigation state isn’t derived from state, but it is explicitly held and pushed to a Navigator object:

UXIconButton(
  icon: Icon(UXIcons(context).book),
  onPressed: () {
    Navigator.of(context)
      .push(platformPageRoute(context: context, 
        builder: (ctx) => DetailPage()
      ));
  }
)

Which is quite similar to what I do in ViewController:

class HomePage: ViewController {
  var view: some View {
    Button("Goto Detail") {
      show(DetailPage())
    }
  }
}

People have mentioned that the current Flutter Navigator may be overly complex. I can’t say much about it, but at least it seems to work reliably, regardless of the situation.

Small Web Service App

As I do, I ended up writing a small Flutter frontend to the Swift Package Index:

It has a TextField, does HTTP and JSON and shows search results in a ListView (did I mention: Flutter documentation 😚).

Important: Don’t use this API in your own apps, get into contact with Sven or Dave if you want to access SPI programmatically.

All the files with small annotations:

main.dart:

import 'package:flutter/widgets.dart';
import 'my_app.dart';

void main() {
  runApp(const MyApp());
}

Moved the app widget to an own file, which gets imported, my_app.dart:

import 'package:flutter/material.dart';
import 'package:hello_flutter/spm_list_page.dart';
import 'uxkit.dart';

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

  @override
  Widget build(BuildContext context) => const UXApp(
    title : 'Welcome to Flutter',
    home  : SPMListPage()
  );
}

Not much magic, SPMListPage is the “ContentView”, spm_list_page.dart:

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_platform_widgets/flutter_platform_widgets.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';    // for jsonDecode
import 'uxkit.dart';
import 'spipackage.dart'; // the 'model'

class SPMListPage extends StatefulWidget {
  const SPMListPage({Key? key}) : super(key: key);

  @override
  State<SPMListPage> createState() => _SPMListPageState();
}

class _SPMListPageState extends State<SPMListPage> { 

  late Future<List<SPIPackage>> _packages; // Swift "lazy"
  final _biggerFont  = const TextStyle(fontSize: 28);
  final _searchField = TextEditingController(text: "Dart");
  
  @override
  void initState() { // like onAppear
    super.initState();
    _fetch();
  }
  @override
  void dispose() {
    _searchField.dispose();
    super.dispose();
  }
  
  List<SPIPackage> _decodeJSON(dynamic json) {
    var packages = <SPIPackage>[];
    final jsonDict = json as Map<String, dynamic>;
    final results  = jsonDict["results"] as List<dynamic>;
    
    for (final result in results) {
      final json = result as Map<String, dynamic>;
      
      if (json["package"] == null) continue;
      final nest = json["package"] as Map<String, dynamic>;
      if (nest["_0"] == null) continue; // Coddable, lolz
      final packageJSON = nest["_0"] as Map<String, dynamic>;
      final package = SPIPackage.fromJson(packageJSON);
      packages.add(package);
    }
    return packages;
  }
  
  Future<List<SPIPackage>> _querySPI(String query) async {
    final response = await http.get(
      Uri(scheme: "https",  host: "swiftpackageindex.com",
          path: "/api/search", 
          queryParameters : { "query": query })
    );
    
    if (response.statusCode == 200) {
      return _decodeJSON(jsonDecode(response.body));
    }
    else {// is this attached to the future?
      throw Exception("Failed to run query! ${response.statusCode}");
    }
  }
  
  void _fetch() {
    setState(() {
      _packages = _querySPI(_searchField.text);      
    });
  }

  // MARK: - UI

  Widget _buildCell(BuildContext context, SPIPackage item) {
    return Container(
      margin     : const EdgeInsets.fromLTRB(16, 16, 16, 0),
      padding    : const EdgeInsets.all(8),
      decoration : BoxDecoration(
        border: Border.all(width: 4, color: Colors.black38),
        borderRadius: const BorderRadius.all(Radius.circular(8)),
      ),

      child: Column( // VStack
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(item.packageName ?? item.repositoryName, style: _biggerFont),
          Text("Stars: ${item.stars}")
        ]
      )
    );
  }

  Widget _buildResultList(BuildContext context, List<SPIPackage> packages) {
    return ListView.builder(
      itemCount: packages.length,
      padding: const EdgeInsets.all(0.0),
      itemBuilder: ( ctx, index ) => _buildCell(context, packages[index])
    );
  }

  Widget _buildResultView(BuildContext context) {
    return FutureBuilder<List<SPIPackage>>(
      future: _packages,
      builder: ( ctx, snapshot ) {
        if (snapshot.hasData) {
          return _buildResultList(context, snapshot.data as List<SPIPackage>);
        }
        else if (snapshot.hasError) {
          return Center(child: Text("ERROR: ${snapshot.error}"));
        }
        else {
          return Center(child: PlatformCircularProgressIndicator());
        }
      }
    );
  }

  Widget _buildSearchForm(BuildContext context) {
    return Row(children: [
      Expanded(child: PlatformTextField(controller: _searchField)),
      UXIconButton(icon: Icon(UXIcons(context).search), 
                   onPressed: () => _fetch())
    ]);
  }

  @override
  Widget build(BuildContext context) => UXScaffold(
    appBar            : UXAppBar(title: const Text("SPI")),
    body              : Column(children: [ 
      Padding(padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child:
        _buildSearchForm(context)
      ),
      Expanded(child: _buildResultView(context)) // expanded is required
    ]),

    iosContentPadding : true
  );
}

I admit, not a beauty, but hey, it is quite OK.

Flutter doesn’t seem to have @Binding’s, so there is the TextEditingController object (a ChangeNotifier!). I don’t actually listen to change events here, but only fetch if the user presses the button.

A point to note is that it is not uncommon to use local methods to construct parts of a UI. Something people do a lot in SwiftUI as well.

The (immutable, all final) “model”, SPIPackage.dart:

class SPIPackage {

  final String  packageId;
  final String  repositoryName;
  final String? packageName;
  final String  packageURL;
  final int     stars;
  final String  summary;

  const SPIPackage({
    required this.packageId,
    required this.repositoryName,
    required this.packageName,
    required this.packageURL,
    required this.stars,
    required this.summary
  });

  factory SPIPackage.fromJson(Map<String, dynamic> json) {
    return SPIPackage(
      packageId      : json['packageId'],
      repositoryName : json['repositoryName'],
      packageName    : json['packageName'],
      packageURL     : json['packageURL'],
      stars          : json['stars'],
      summary        : json['summary']
    );
  }
}

Note the optionals. No idea why the “factory” is a separate concept from the constructor. The dynamic values of json don’t have to be casted explicitly.

That’s it!

Closing Notes

First of all, many thanks go to @pfriedrich_ for helping us to get started w/ Flutter!

Developing in Flutter seems enjoyable from a developer perspective. Hot reload, great documentation, actually working tooling is just excellent.

What I generally dislike is the “looks”. VSCode is as practical as it is ugly. The apps that Flutter produces are ugly (@kiliankoe always asks for a single Flutter app that doesn’t feel crap on iPhone). The source code is ugly if you use actual Flutter conventions (trailing commas everwhere, comments injected on closing braces, …).

But well, the API/language is OK. If I would be forced (💰💰💰💰💰) to produce an Android app, Flutter seems like a decent option (though Kotlin/Jetpack Compose sound interesting as well).
SwiftUI developers will find a lot to like (but risk hating Xcode even more).

While it is always hard to talk about performance without doing decent tests, it feels like SwiftUI has the potential to be dramatically faster (10×?). Given all the static typing, stack allocation, per-view invalidation. But who knows, maybe the Dart compiler can compensate for all that 🙅‍♀️

As usual all feedback is welcome: @helje5 or me@helgehess.eu.

Contact

Feedback is warmly welcome: @helje5, @helge@mastodon.social, me@helgehess.eu. GitHub.

Want to support our work? Buy an app: Past for iChat, SVG Shaper, Shrugs, HMScriptEditor. You don’t have to use it! 😀

Written on June 6, 2022