When I looked at
Tauri last week, I was asked how it compares with other similar app-building projects.
Flutter is more of a platform and uses the Dart language, but I think with Tauri supporting mobile builds it might be time to look at where Flutter is right now.
Flutter is “an open source framework by
Google for building beautiful, natively compiled, multiplatform applications from a single codebase.” It can compile down to ARM for full mobile support. It focuses on adaptive control of the screen.
Their playpen is called
Dartpad and you can look at Dart (the logic code) and Flutter (the widget model) examples. Initially, I would assume that in the same way you can make calls to Rust within the web view in Tauri, you can make calls to Dart while in Flutter. Obviously one has to balance that unlike JavaScript and Rust, the two Google languages will have less general use. But Dart and Flutter fit together smoothly as they are fully integrated.
This week I won’t hit the “Get started” button, but will drill down into the
Sunflower example, as it summarizes perfectly what Flutter is offering. You can just run it directly from the link, which loads it into Dartpad.
👁 Image
The 250 seeds fly from the center to the circumference, or the other way around, reacting to the slider. So some are in the middle, the rest on the edge. It is clear there is some animation as the seeds seem to float serenely.
So from a design point of view, we have three models to consider. There is the structural model (where things are in the app and on-screen), the mechanical model (how it works logically) and the interaction model (responding to events).
Structurally, we have the title “Sunflower” at the top; we have the sunflower in the middle, composed of a dotted circle of seeds and the seeds in the center. Then we have the slider at the bottom with the text showing the number of seeds in the center (from a solitary seed at the left end to all 250 as we slide to the right end).
We know we will need math to calculate the position of the seeds on the circle, and their trajectory from and to the middle.
While there are a few ways we could represent this, it would seem reasonable to have an object for each seed, representing its position, and maybe its trajectory. Then we need an object to represent the sunflower’s state. And we vaguely understand that the slider will be some form of built-in control.
OK, let’s now take a look at the basic class outlines from the code on the left side of the Dartpad. As long as you are familiar with classes, objects and JSON layout, there is nothing weird in the code. In fact, if you’ve used a CSS framework like Bootstrap, you should feel at home.
import 'dart:math' as math;
import 'package:flutter/material.dart';
const int maxSeeds = 250;
void main() {
runApp(const Sunflower());
}
class Sunflower extends StatefulWidget {
...
}
class _SunflowerState extends State<Sunflower> {
...
}
class SunflowerWidget extends StatelessWidget {
...
}
class Dot extends StatelessWidget {
...
}
We see a bit of boilerplate with a
main function running the app. We see that the
Sunflower class inherits from
StatefulWidget, and this class seems to also have the responsibility to hold everything for the app, as it is passed into the
runApp method.
We see what looks like an internal
State model for
Sunflower, and a class for the
SunflowerWidget (which is a stateless widget), and another stateless widget for the
Dot (the seeds). It isn’t immediately clear how the responsibilities are divided at this stage. Note the
maxSeeds value is set to 250.
So let’s pick the code apart like Lego, searching for the simplest elements we know must be present. First of all, we noted that the app has the title ‘Sunflower’. Where is that?
class _SunflowerState extends State<Sunflower> {
int seeds = maxSeeds ~/ 2;
@override Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
brightness: Brightness.dark,
appBarTheme: const AppBarTheme(elevation: 2),
),
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: const Text('Sunflower'),
),
...
So we see that the
_SunflowerState class is actually setting up (or
building) the app, and the
AppBar contains a title, built of a
Text object containing the title we recognized. This is within a
Scaffold object.
The next simplest thing we know is that there is a slider. Where is that? Well, that is defined immediately after the
AppBar within the body of the scaffold, within a
Column object, as the child of a
SizedBox:
... body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: SunflowerWidget(seeds),
),
const SizedBox(height: 20),
Text('Showing ${seeds.round()} seeds'),
SizedBox(
width: 300,
child: Slider(
min: 1,
max: maxSeeds.toDouble(),
value: seeds.toDouble(),
onChanged: (val) {
setState(() => seeds = val.round());
},
),
),
...
We see that the
seeds variable, defined in the first list above, is actually set to half the
maxSeeds — and indeed when you run the app it starts with half the seeds in the middle, and half on the circumference.
In fact the line…
int seeds = maxSeeds ~/ 2;
…has the only explicitly weird thing in the code — a
truncating division operator to round off the result of dividing by 2.
The
Slider object itself maps its minimum and maximum values from 1 to the
maxSeeds value. Its start position or value is set to the
seeds variable. We see that the text above the slider is just a
Text element using that seed value. And we now know something else — this code must be re-evaluated to work. And that is clearly done from the
onChange event listener, which resets the value of seeds based on the slider’s position mapping values. If you run with a maxSeeds of 500 instead of 250, you see exactly what you expect.
Now we know there must be some maths. Where is that?
...
class SunflowerWidget extends StatelessWidget {
static const tau = math.pi * 2;
static const scaleFactor = 1 / 40;
static const size = 600.0;
static final phi = (math.sqrt(5) + 1) / 2;
static final rng = math.Random();
final int seeds;
const SunflowerWidget(this.seeds, {super.key});
@override Widget build(BuildContext context) {
final seedWidgets = <Widget>[];
for (var i = 0; i < seeds; i++) {
final theta = i * tau / phi;
final r = math.sqrt(i) * scaleFactor;
seedWidgets.add(AnimatedAlign(
key: ValueKey(i),
duration: Duration(milliseconds: rng.nextInt(500) + 250),
curve: Curves.easeInOut,
alignment: Alignment(r * math.cos(theta), -1 * r * math.sin(theta)),
child: const Dot(true),
));
}
...
If you know the equation for the circumference of a circle, but are not sure what “tau” has to do with it,
check this out. But our only job here is to see where the maths is done. We can also see that we create a
Dot object (the seed represented graphically) for each seed with a simple
for loop. And the Dots are clearly held in the
seedWidgets array. You can also see the trappings of the animation settings, including the path taken and time taken — if you have done any animation work you will recognize the
easing value.
Conclusion
Hopefully, you can see from this post how Flutter compares with Tauri. Clearly the Dart language works within the widget Flutter models very tightly, to enable far greater control of what is on screen. We can tell a lot from the imports; the app only required the Dart math package and the Flutter materials for the widgets. We are being asked to think in terms of widgets — because the widgets are very much first-class objects, and the app is built around their interaction. We set up the initial conditions of the widget, and how they respond to change. The default behavior can be overridden to get what we need.
Without further examination, we can see that Flutter is giving us very tight control of what is on screen, very much like a UI framework. If this is what you need, Google has you covered here.