Flutter Without Flutter
This is an experiment. Let’s create the minimal code required to display something on a mobile device without using the Flutter framework.
Flutter is a high level GUI framework that uses dart:ui
(an abstraction of the Skia engine) to actually display something and to interact with the platform that something is displayed on. Of course, we’re free to directly use this low-level dart:ui
library our selves.
Setup
Let’s create a new Flutter project:
$ flutter create flutter_without_flutter
then replace `lib/main.dart` with this code:
import ‘dart:ui’;void main() {
window.onBeginFrame = beginFrame;
window.scheduleFrame();
}void beginFrame(Duration duration) {
}
Explanation: We register a global function called beginFrame
with the window
singleton and then ask the graphics engine to schedule a callback to this function. It is supposed to prepare and eventually draw a frame, that is, display something on the device screen.
Drawing Something
Because I’m used to work with logical units instead of physical pixels, the first step in beginFrame
is to convert the physical screen size of the device to more familiar values:
void beginFame(Duration duration) {
final pixelRatio = window.devicePixelRatio;
final size = window.physicalSize / pixelRatio;
final physicalBounds = Offset.zero & size * pixelRatio;
…
The next step is to setup a Canvas
that uses logical units:
…
final recorder = PictureRecorder();
final canvas = Canvas(recorder, physicalBounds);
canvas.scale(pixelRatio, pixelRatio);
…
Let’s then draw a circle filled with red-ish paint:
…
final paint = Paint()..color = Color(0xFFF44336);
final center = size.center(Offset.zero);
canvas.drawCircle(center, size.shortestSide / 4, paint);
…
The last step is to take the recording created by calling Canvas
methods and use it to build a so called scene that is then rendered by the window
singleton:
…
final picture = recorder.endRecording();
final sceneBuilder = SceneBuilder()
..pushClipRect(physicalBounds)
..addPicture(Offset.zero, picture)
..pop();
window.render(sceneBuilder.build());
}
By the way, the usual instant hot code reloading of Flutter projects still works and this is why it might be useful to use this setup to create low level graphics applications like for example 2D games.
Moving Something
To move the circle and bounce it around the screen, we will store its center point in a global variable (called center
) and its velocity in another globale variable (called velocity
), modify these variables each time beginFrame
is called and then ask for another draw operation using scheduleFrame
at the end of the beginFrame
function.
Here is the relevant change:
Offset center;
Offset velocity;void beginFrame(Duration duration) {
…
canvas.scale(pixelRatio, pixelRatio);final radius = size.shortestSide / 4;
if (center == null) {
center = size.center(Offset.zero);
velocity = Offset(3, 5);
} else {
if (center.dx < radius || center.dx > size.width — radius)
velocity = velocity.scale(-1, 1);
if (center.dy < radius || center.dy > size.height — radius)
velocity = velocity.scale(1, -1);
center += velocity;
}final paint = Paint()..color = Color(0xFFF44336);
canvas.drawCircle(center, radius, paint);…window.scheduleFrame();
}
Because we don’t have control over how often the beginFrame
function is scheduled, we should probably make use of the passed duration
and bind the circle’s velocity to the passed time. To compute the time that passed between calls, we introduce another global variable called lastDuration
.
Duration lastDuration;void beginFrame(Duration duration) {
…
if (center == null) {
…
} else {
…
final delta = (duration — lastDuration).inMilliseconds / 1000;
center += velocity * delta;
}
lastDuration = duration;…
If you kept the application running and just changed the soure code, the circle should have started moving just like magic.
Reacting to Touches
Last but not least, let’s change the velocity each time we tap the screen. To detect touches, we need to add another callback called onPointerDataPacket
. This function receives a list of PointerData
objects which describe touch down, touch move, and touch up events. We’re only interested in events of type PointerChange.up
, though.
Here is the new main function:
void main() {
window.onBeginFrame = beginFrame;
window.onPointerDataPacket = pointerDataPacket;
window.scheduleFrame();
}
And here is the new callback function:
void pointerDataPacket(PointerDataPacket packet) {
for (final data in packet.data) {
if (data.change == PointerChange.up) {
velocity = Offset.fromDirection(
_random.nextDouble() * pi * 2,
_random.nextDouble() * 800–400,
);
}
}
}final _random = Random();
Using this foundation, it should be possible for you to create your own Flutter-like GUI framework from scratch.
I leave this as an exercise to the reader :-)