Flutter: Through the abstractions forest 🌲
Do you remember `div.innerHTML = “Hello World”`? You can do it in “Flutter” too if you really want to.
Due to the good layer architecture “Flutter” allows writing code in different styles. But before we start coding we should have a picture of how it works now by default and after that removes some magic abstractions and dives one level deeper.
Flutter has three main layers:
- 🍏 “Widgets”
- 🍋 “Render”
- 🍆 “dart.ui”
Each layer relies on its previous layers, so “Widgets” layer interacts with “Render” and “dart.ui” layers. Accordingly “Render” layer interacts with “dart.ui” and knows nothing about “Widgets”. Yes, that it’s how properly implemented layer architecture looks like.
Now let’s add a short description for each layer:
- 🍆 “dart.ui” — Low-level API to interact with the render engine. Just a plain canvas and a set of functions to render lines and dots on it.
- 🍋 “Render” — This layer is very similar to “DOM”(Document Object Model) in the web. You have a tree of mutable render objects.
- 🍏 “Widgets” — This layer is similar to virtual “DOM”. Tree of immutable widgets.
By default “Flutter” creates a project with exposed “Widgets” layer:
void main() => runApp(new MyApp());
It’s a good choice in most cases because each layer hides some implementation details and provides clean and powerful abstractions which allow writing complex applications quickly. But, it’s not for free — every layer adds the computation cost and a set of abstractions which doesn’t fit very well for some kind of apps. A good example of those is a game app where the speed is critical and “Widgets” model is clumsy. Though, to be open for an extension without the framework modification is better than not to.
Now it’s time to code a simple counter app without “Widgets”.
- First things first, run `flutter create counter` in a terminal/console.
- Open “lib/main.dart” and remove all code.
- Import “flutter/rendering.dart” package which provides a set of render objects and functions related to “Render” layer.
import 'package:flutter/rendering.dart';
4. Now write our entry point — the function “main”.
import 'package:flutter/rendering.dart';void main() {
}
5. Now instead of `runApp(MyApp())` we’ll use `RenderingFlutterBinding(root: rootNode)`. This function attaches the “rootNode” to the render view. As the “rootNode” we take “RenderParagraph” object and print “Hello World” on the screen.
import 'package:flutter/rendering.dart';void main() {
final rootNode = RenderParagraph(
TextSpan(text: "Hello World!"),
textDirection: TextDirection.ltr,
); RenderingFlutterBinding(root: rootNode);
}
Now we can start/“hot restart” our app and see the result. “Hello World!” text should appear in the top left corner.
6. Let’s center our text on the screen by “RenderPositionedBox”.
import 'package:flutter/rendering.dart';void main() {
final rootNode = RenderParagraph(
TextSpan(text: "Hello World!"),
textDirection: TextDirection.ltr,
); RenderingFlutterBinding(
root: RenderPositionedBox(child: rootNode),
);
}
After the restart, the text should be centered on the screen.
At this point, all steps look the same as if we were writing code using “Widgets”. To understand the difference between “Render” objects and “Widgets” let’s change the text when a user taps on it.
7. To handle a tap I’ll use “RenderPointerListener”.
import 'package:flutter/rendering.dart';void main() {
final rootNode = RenderParagraph(
TextSpan(text: "Hello World!"),
textDirection: TextDirection.ltr,
); RenderingFlutterBinding(
root: RenderPositionedBox(
child: RenderPointerListener(
onPointerDown: (_) => rootNode.text = TextSpan(text: "Hi"),
child: rootNode,
),
),
);
}
As you can see to change the text from “Hello World” to “Hi” we just update/mutate “text” property of “rootNode” object. Under the hood, “RenderParagraph” will notify a render engine that the “text” property was changed and we need to redraw the screen. The render engine, in its turn, will pick up the appropriate time for redraw. So the main difference between “Render” objects and “Widgets” is that “Render” objects are mutable and “Widgets” are not.
8. Now it’s not that hard to imagine how to write a simple counter app.
import 'package:flutter/rendering.dart';void main() {
int counter = 0; counterText(counter) {
return TextSpan(text: "You have pushed the text this many times: $counter");
} final rootNode = RenderParagraph(
counterText(counter),
textDirection: TextDirection.ltr,
); RenderingFlutterBinding(
root: RenderPositionedBox(
child: RenderPointerListener(
onPointerDown: (_) => rootNode.text = counterText(counter += 1),
child: rootNode,
),
),
);
}
That’s it. Thanks!