This is the second day of my participation in the First Challenge 2022

This article is translated from the official 👉Inside Flutter. This article introduces the internal mechanism of Flutter, from combination-first design, to the principle of efficient operation of node tree, to the handling of slippage. You can see why Flutter is a widget that works efficiently.

Here is the text


This document describes the internal working mechanism of Flutter. Fluttter can run efficiently because of these mechanisms. Flutter widgets are built in a positive way, so we often see a large number of widgets in the UI. To enable active composition, Flutter uses sublinear algorithms in its layout and construction, and a data structure that works with it. In addition to these two features, Flutter also has some constant factor optimizations that make tree operation more efficient. With a few extra details, callbacks make it easy to create a wirelessly scrolling list, and callbacks can be used to precisely build widgets that need to be displayed.

Aggressive composition — positive composition

One of the most distinctive aspects of Flutter is its aggressive composition — positive combination. Widgets are built by combining other widgets, and these other widgets are also built from more basic widgets. For example, the Padding is a Widget, not a property of another Widget. Thus, the UI of Flutter is many, many more widgets than are explicitly developed in the developer code.

The Widget builds recursively to the bottom of the RenderObjectWidgets, which is also a Widget that builds nodes in the Render tree. The Render tree is a data structure that stores UI geometry that is computed during the Layout phase and used in the painting and hit testing phases. Normally, Flutter developers don’t need to write and develop render objects directly, just use the widgets that match them.

To support this Widget layer combination, Flutter uses several efficient algorithms and mechanisms on all three trees, which are described below.

Sublinear layout

Due to the large number of widgets and Render objects, the key to high performance is efficient algorithms. The most important aspect of performance is layout, which we know determines geometric information such as size, location, and so on. Some other UI frameworks have an O(N ^ 2) layout algorithm, or worse. The goal of Flutter is linear overhead for initial layouts and sublinear overhead for subsequent updates. In most cases, you’re updating an existing layout, because you only initialize it once. In general, less time should be spent on layout than on rendering.

A Flutter performs a layout every frame, and the layout algorithm has only one input parameter — Constraints. The Constraints are passed from the parent node to the child node, or to each child node if there are more than one. The child nodes also recursively perform their own layout and return geometry up the tree at 👆. Importantly, as long as a rendered object has been returned from its layout method, it will not be visited again until the next frame. This layout method does two things: first, it accepts the constraint information passed in from the parent node, and second, it completes itself and returns the parent node its own collection information. This allows each render object to be accessed at most twice during the layout: once to pass constraints down and once to pass geometry up.

Flutter also has some properties of the general layout protocol described above. The most commonly used feature is RenderBox, a class that defines two-dimensional Cartesian coordinates. In a box layout, constraints are defined by four values: maximum minimum width, maximum minimum height. During layout, child nodes need to determine their own geometric information within the constraints given by their parents. After the child layout is complete, the layout method returns, and the parent node determines the position of the child node in the byte coordinate system. Note that the layout of the child node does not depend on its position, because the position is determined after the layout. Therefore, the parent node is free to place its children multiple times without rearranging them.

In general: During layout, the only information flowing from parent to child is constraint, and the only information flowing from child to parent is geometric information. Such rules and data uniqueness reduce layout effort:

  • If the child node does not mark its own layout as dirty, the child node can be returned immediately from the layout method. And as long as the parent node gave the same constraints as the previous layout, it will return immediately.

  • Whenever the parent node invokes the child node’s layout method, the parent node needs to indicate whether it uses the geometry information returned by the child node. If the parent node does not use the size information of the child node, the parent node will not recalculate the layout even if the size information of the child node changes, because the parent node has guaranteed that the child node size is within the constraint.

  • Tight constraints mean that geometric information is fixed and unique. For example, the minimum width is equal to the maximum, and the minimum height is equal to the maximum. So size is the only thing that makes width bigger than height. If the parent gives the child a Tight constraint, when the child is rearranged, the parent does not need to reevaluate the layout information because the child does not change size.

  • A render object can determine its own geometry information based solely on the constraints of its parent node. This tells the framework that when rendering objects are rearranged, their parent nodes do not need to be rearranged, even if the constraints passed by the parent node are not tight, and even if the parent layout depends on the size of the child nodes. Because in this case, the size of the child node only changes because of the constraints on the parent layout.

Because of these optimizations, when some nodes in the Render tree are marked as dirty, only those dirty nodes and their limited child nodes need to be laid out.

Sublinear widget building

Similar to the layout algorithm, the Widget building algorithm of Flutter is sublinear. After the build, the Widget nodes are held by the Element tree, and the widgets retain the logical structure of the UI. The Element tree is necessary because the widgets themselves are immutable, and their immutability means that they do not retain parent-child relationships with other nodes. The Element tree also holds the state object for the StatefulWidget.

An Element can become dirty in response to user input or other events, for example, if the developer calls the setState() method. The framework keeps a list of dirty elements and skips clean elements during the build phase to build directly into the dirty list. During build, the flow of information through the Element tree is one-way, which means that each Element is accessed at most once during build. During build, an Element is not marked as dirty again as long as it is clean, because its ancestor nodes are clean.

Because widgets themselves are immutable, if an Element is not marked as dirty, then when the parent Element node recreates its child Element with the same Widget that is authenticated, Element can be returned immediately from build. In addition, Element simply compares the identity of the old and new widgets to determine whether the two widgets are the same. Developers use this mechanism to implement the Reprojection pattern, where a Widget wraps a pre-loaded member variable Widget and uses the member variable as part of the build method.

During build, Flutter tries to avoid directly traversing the chain of the parent node, using InheritedWidgets. If the Widget traverses the parent node’s chain directly, the build complexity is O(N²), where N is the depth of the tree. To avoid such parent chain traversal, the framework layers information down the Element tree, which is a hash table of an InheritedWidget held in the Element. In general, the tables that Element references are the same, and if an InheritedWidget is an InheritedWidget, the underlying Element’s table has one more Element than its parent’s table (its own InheritedWidget).

Linear consistent

The way that Flutter decides whether or not to reuse elements differs from the mainstream solution, which uses tree-Diffing algorithm. Instead, Flutter checks independently for each child node. In this case, the algorithm complexity is O(N). The sub-node linear consistency algorithm optimizes the following cases:

  • The old child node list is empty
  • The two lists are the same
  • Insert and delete widgets at the locations identified in the list
  • The old and new lists contain a Widget of the same type with the same Key

A common way to compare widgets is to compare the types and keys of the old and new lists from start to finish, and you might find a non-empty range in the middle of each list that contains all the mismatched sublists. The framework then places the old child nodes in the hash table based on the Element’s Key. The framework then generates new Element nodes sequentially, looking for elements with the same key from the previous hash table, using the new Widget to generate a new Element if none is found, and performing the subsequent lifecycle process. If found, the new Element is reused.

Tree separation

Because Element holds two key pieces of information: the state of the StatefulWidget and the render object behind it, the Element reuse mechanism is important for overall performance. Because Element can be reused, both the logic of the UI and the calculated layout information in State can be reused, eliminating the need to iterate through the rendering of the entire subtree. In fact, the reuse mechanism is so valuable that Flutter supports non-local tree changes based on it.

Developers can use the GlobalKey mechanism to implement non-local tree changes. Widgets can set Key properties, one of which is GlobalKey. Each GlobalKey is unique within the application and is registered in the hash table of the specified thread. During layout, developers can place the entire sub-tree of GlobalKey’s Widget anywhere in the Element tree. The framework checks the hash table and treats the level above the new location as its new parent, preserving the entire subtree rather than recreating it.

Similarly, render objects are affected by the way Element reassigns its parent, in which case the render objects retain their layout information because there is no change in the layout information flowing from parent to child nodes. The new parent node is marked as dirty because its children have been replaced, but if the new parent node has the same constraint information as the old parent node, the child node layout process is immediately completed and geometry information is returned.

Developers often use GlobalKey and non-local tree changes to achieve effects such as HERO switching and Navigation routing.

Constant – factor optimization

In addition to these algorithmic optimizations, the implementation of combinatorial priority mechanisms also relies on some constant-factor optimizations. Based on the main algorithm, these optimizations are the most important.

  • Child-model agnostic. Unlike most toolkits, which use child lists, Flutter’s render tree does not commit to a specific child model. For example, Flutter’s render tree does not commit to a specific child model. the RenderBox class has an abstract visitChildren() method rather than a concrete firstChild and nextSibling interface. Many subclasses support only a single child, held directly as a member variable, rather than a list of children. For example, RenderPadding supports only a single child and, as a result, has a simpler layout method that takes less time to execute.

  • Most UI frameworks specify that the Child node is an array. The render tree of Flutter does not specify what the Child model is. For example, RenderBox’s abstract method is visitChildren(), instead of the instantiated firstChild and nextSibling. Many subclasses have only one child node, and the member variable holds the child node directly. For example, RenderPadding has only one child node, so the layout method takes less time to process.

  • In Flutter, the render tree is device independent and within the visual coordinate system. That is, even if the current reading direction is from right to left, render’s X-axis gets smaller and smaller to the left. The Widget tree is a logical coordinate system, meaning that the start and end values depend on the interpretation of the reading method. The conversion between the visual coordinate system and the logical coordinate system is done between the Widget and Render trees. Rendering tree layout and rendering computes more frequently than widget-to-render tree switches, thus avoiding repetitive coordinate transformations, so this approach is very efficient.

  • Most render objects are insensitive to the processing of text, which is processed by a specific RenderParagraph, which is a leaf node of the render tree. Developers combine this node into their UI using composition rather than subclassing RenderParagraph. The advantage of this approach is that RenderParagraph avoids double-counting as long as the constraints on the parent nodes are the same.

  • Observable Flutter uses both observer mode and responsive programming. Although reactive is dominant, Flutter uses observer mode for some leaf nodes. For example, the Animation notifies all listeners when the value of the Animation changes. Flutter passes these viewable objects from the Widget tree to the render tree. When values change, objects in the render tree respond directly to the changes, regardless of the render pipeline. For example, changes to the Color Animation
    only change the draw phase, not the build + draw phase.

All of these work together to make the preferred combination mechanism more efficient and efficient.

The Element tree is separated from the RenderObject tree

RenderObject is homomorphic to the Element tree, or rather, a subset of the Element tree. An obvious simplification would be to join the three trees together, but it would actually be more beneficial to separate the three trees:

  • Performance When the layout changes, only the parts that need to be changed need to be traversed. Because of composition, the Element tree has many additional nodes that need to be skipped.
  • The decoupled Widget protocol runs according to its needs, and the Render protocol works according to its needs, making it more simple and reducing bug risk and unit testing.
  • Type safe Because the Render object guarantees that the child nodes are of the specified type at runtime, the Render object is more type safe. Widgets may not know the coordinate system at the time of layout, for example, the same Widget can be used in either a box layout or a Sliver layout. Also, in the Element tree you can find render nodes of the specified type along the tree.

Wireless rolling

As you all know, wireless scrolling lists are a very complex and important requirement for any framework. Flutter is based on the Builder model and uses a straightforward API to accomplish this. The ListView uses builder callbacks to make the Widget appear in the window as the user scrolls. Behind them is the Viewport-aware layout and on-demand loading.

Viewport – aware layout

Like most Flutter situations, the scroll Widget is built using combinations. Outside the scroll Widget is the Viewport component, which allows the internal component to be larger than the external component. That is, although the interior is very large, you can scroll inside the view. The children of a Viewport are not renderBoxes, but specific RenderSlivers. RenderSliver has its own layout protocol.

The Sliver layout protocol is structurally the same as the Box layout protocol, transferring constraints from top down and collection information from bottom up. However, the inputs and outputs are different. Constraints in the Sliver layout protocol include information such as the size of the remaining visual space. The geometry information implements the associated scrolling effects, including folded heads, parallax, and so on.

Different slivers fill the remaining visual space in different ways. For example, a Sliver that is a linear list of children places the children in order until the children run out or space is full. Similarly, a child node is a Sliver of a two-dimensional table that fills only the visible part of the table. Since Sliver knows how much space is left, it sets an infinite number of children, or it can construct an appropriate number of children.

Slivers can be combined to achieve custom scrolling layouts and effects. For example, a single Viewport can be used for folding headers, plus linear lists and tabular lists. The layout protocol allows slivers to coordinate with each other to construct only the child nodes that actually need to be displayed on the screen, avoiding complex decisions about whether the child nodes are headers, lists, or grids.

Build widgets on demand

The previously mentioned infinite list would be cumbersome to implement if the Flutter were strictly build-then-layout-then-paint pipeline. Because information about visual space is only available during the layout phase. Without other mechanisms, the layout phase is too late to build space-filling widgets. Flutter solves this problem by using staggered build and layout phases. At any point during the drawing phase, the Framework can start building new widgets on demand, as long as the new Widget is a descendant node of the render object currently being rendered.

The reason why the build and layout stagger can be realized is that the build and layout algorithms strictly control the information. In particular, during the Build phase, information can only be propagated downwards. When a render is performing a layout and the layout has not traversed a child node, that child node is not affected. Similarly, as long as the layout is complete, the render object cannot be accessed again, which means that the write operation does not affect the constraint information used to lay out the byte points.

In addition, linear coordination and tree decomposition are critical for updating elements efficiently during scrolling and updating rendered trees efficiently when elements roll in and out of the viewport edge.

In addition, linear coordination and tree manipulation are essential for efficiently updating elements during scrolling and modifying the render tree when scrolling elements enter or exit the view at the edge of the viewport.

Ergonomics in API

Efficiency matters only if the framework can be used effectively. To make the Flutter API design easier to use, Flutter has been tested repeatedly with developers from a broad UX perspective. This work sometimes validates previous design decisions, sometimes helps prioritize features, and sometimes changes the direction of API design. For example, the Flutter API has many, many documents. UX affirms the value of documentation and emphasizes the importance of sample code and algorithm instructions.

This section focuses on some points that make the API easier to use.

Heart-to-heart API for developers

The base classes of Flutter nodes are: Widget, Element, and RenderObject. These are the nodes of the three trees, but there is no specific child model defined. This allows each node to define its own child model.

Most Widget objects have only one child node Widget and therefore only expose the child parameter. Some widgets require multiple child nodes, thus missing the children parameter of the array type. Some widgets also do not have any child nodes and do not retain memory, so no parameters are set. Similarly, RenderObjects also exposes specific APIS as child nodes. The RenderImage is a leaf node and has no concept of child nodes. RenderPadding requires a child node, so it has a child variable. RenderFlex needs to manage an array of any number of children, and it leaks array parameters.

In special cases, more complex child models are required. RenderTable renders children as a table, so its constructor requires an array of arrays. This class also exposes getters and setters to control rows and columns, and has specified methods to replace individual elements positioned by X and Y, add a row, add a new set of children, Replace the entire child with an array and column number. Implementationally, the render object does not use a list array, but an index array. Most render objects use linked list arrays.

Chip components and InputDecoration objects have fields that match slot points. Slots exist on components. A universal child size model is mandatory Semantics is the first element of the child array. For example, defining the first child as a prefix value and the second as suffix, a specific child model allows specific naming attributes.

Because of the flexibility of Flutter, the nodes on the three trees can operate with their own behavior. We generally don’t want to insert one element in a table and cause all the other elements to be wrapped. Similarly, we typically remove Flex’s child nodes by reference, not by index.

RenderParagraph is the most extreme case: its byte points are of a completely different type — TextSpan. Within the scope of RenderParagraph, a TextSpan tree is formed.

Specialized apis that meet developer expectations are not just child models, but other designs as well.

Flutter has many widgets that developers can use to solve some problems. Knowing how to use Expanded and SizedBox components makes it easy to add a gap to a row or column, which is unnecessary. The same effect can be achieved by finding the Spacer component when we search for space.

Similarly, hiding a Widget subtree is easy to implement, as long as we don’t write them to our tree. However, the developer wants to achieve this effect through a component. Visibility is to achieve this effect, as long as you use Visibility to wrap the subtree.

Straightforward arguments

UI frameworks have many attributes, and it’s hard for developers to remember what each attribute of each constructor means. Flutter uses a responsive development framework, with many constructor calls in the build method. With Dart named parameter support, the Flutter API makes the build method straightforward and easy to understand.

This pattern extends the case of multiple arguments to methods, especially Boolean arguments, where most of the true or false Settings are documented and easily understood. Also, to avoid semantic confusion, Boolean parameters and attributes are named opposite, such as enabled: true instead of disabled: false.

bridgepit

Defining apis is a technique used everywhere in the Flutter framework, such as non-existent error conditions and so on. This removes the entire error class from consideration.

For example, interpolation functions allow null values at both ends of interpolation, rather than defining an error case: values that are null at both ends are always null, starting from null is equivalent to starting from 0, and going to NULL is equivalent to going to 0. This means that a developer passing null to a differential function will get a reasonable result and no errors will occur.

A more subtle example is Flex’s layout algorithm. The idea is that the space of a given Flex rendering object is divided by its children, so flex’s size takes up the entire available space. In the original design, providing an infinite boundary would have failed because Flex is infinite in this case, which would have been a useless configuration. The DESIGN of the API is adjusted so that while Flex’s constraint is infinite, Flex’s size is adjusted to the child node gate size, thus reducing error situations.

This approach is also used with constructors, where the type of argument required by the constructor is different from the type of argument passed to it. For example, the PointerDownEvent constructor does not allow the Down property to be set to false, so the constructor does not set the Down field and always sets down to true.

In general, this approach defines a valid interpretation of the input. The simplest example is the Color constructor. Instead of defining four input integer parameters (one red, one green, one blue, and one transparency, and defining the correct range for each value), the default constructor simply defines an integer value and defines the meaning of each byte. Therefore, any input is a valid color value.

A better example of design is the paintImage() method. This method takes 11 parameters, each with its own field. But they are carefully designed to be orthogonal to each other, so that there are very few invalid combinations.

Error reporting priority

Not all error conditions can be designed. For the rest of the errors, Flutter usually catches them as early as possible in Debug mode and reports them immediately. Assert – Assertions are widely used. Constructor arguments are scrutinized and the life cycle monitored. When they detect an inconsistency, they immediately throw an exception.

In some use cases, this can be extreme: for example, when unit testing, RenderBox subclasses check to see if constraints are satisfied regardless of what is being tested. This helps with errors that might not be executed.

When an error is thrown, it can contain a lot of usable information. Some Flutter errors print stack information and try to locate the actual Bug location. Some will traverse the tree looking for the wrong code. The most common errors include detailed instructions, in some cases including sample code to avoid errors, or links to further documentation.

Responsive mode

Binary access leads to a problem with mutable tree-based apis: the initial state of the tree created is often different from subsequent update operations. The rendering layer of Flutter uses this responsiveness because it allows efficient maintenance of the persistent tree, which also allows efficient layout and rendering. However, this means that interacting directly with rendered objects is laborious and bug-prone.

The Widget layer of Flutter introduces composition and uses responsiveness to manipulate the rendering tree behind it. The API abstracts tree operations by combining tree creation and updates into a single build. When the state of the application changes, a new UI configuration (Widget) is created, and the framework calculates the UI that should be rendered (Element and Render tree calculations) in response to the Widget changes.

The interpolation

Because the Framework of Flutter encourages developers to describe UI configurations based on the state of the application, there is a hermitage mechanism to animate between these configurations.

For example, suppose that the UI corresponding to S1 state is round, and the UI corresponding to S2 state is square. Without an animation mechanism, the UI would change very abruptly when the state changes. Because of the food animation, the changes are very smooth in a few frames.

Implicit animation is characterized by a StatefulWidget that holds the value of the current animation, the time the animation started, and the scope of the animation. And can complete the conversion from the current value to the next value.

The implementation of this animation relies on the LERP method. Each state represents an immutable object that has the appropriate configuration (color, line width, and so on) and knows how to draw itself. During the animation, intermediate steps called transition effects are drawn, and the start and end values and time point t are passed to the lerp function method, which then returns an immutable object that represents the transition effects of the animation.

This is achieved by using the “Lerp” (linear interpolation) function, which uses immutable objects. Each state (circle and square in this case) is represented as an immutable object that is configured with the appropriate Settings (color, stroke width, and so on) and knows how to draw itself. It’s time to draw animation intermediate steps, start and end values passed to the appropriate “insects honey” function and * t * value represents the point along the animation, of which 0.0 says represents the “start” and 1.0 “end” [8] (docs. Flutter. Dev/resources/I… A8), the function returns a third immutable object representing the intermediate stage.

For the transition from a rounded circle to a square, the LERP method returns an object representing rounded square, which is the size of rounded corner at time T. The lERP method of the color interpolator returns an immutable object representing the color. Similarly, line-width interpolators return immutable objects representing the width of a double. Whether representing colors or shapes, these objects implement the same interface and can be used for drawing.

This technique completely decouples the state mechanism, the mapping of the state to the UI, the animation mechanism, the interpolation mechanism, and the logic mechanism of drawing.

This method is widely used. In Flutter, basic types such as Color and Shape can be interpolated, but complex types such as Decoration, TextStyle, and Theme can also be interpolated. Complex types are often built from components that can interpolate, and the interpolation of complex types can be as simple as the primitive types, whose recursive differences achieve the effect of complex objects.

Some interpolable objects are defined by a fixed class system. For example, the ShapeBorder interface represents shapes and has some shapes already written: Rectangular border, BoxBorder, CircleBorder, round rectangular border and StadiumBorder. A single LERP method cannot know all possible types in advance, so the interface replaces the definition of lerpFrom and lerpTo methods. When told to interpolate from shape A to shape B, it first checks whether B can be changed by lerpFrom A, and if not, it then checks whether A can lerpTo to B. (If neither is possible, then t less than 0.5 returns A and t greater than 0.5 returns B)

In this way, the class hierarchy can be expanded arbitrarily, interpolating arbitrarily between two values.

In some cases, the interpolator itself cannot be described by an available class, and a private class is defined to describe the differential process. Take the difference between the CircleBorder and the round rectangleborder.

This interpolation mechanism has another benefit: it can handle the difference from the process to the new value. For example, in the middle of a circle to square transition, the animation inserts a triangle and the shape can be changed again. Transitions can be performed seamlessly as long as the triangle class is lerpFrom Rounded – Square.

conclusion

The Slogan of The Flutter is “Everything is a widget”. The UI is implemented by composition, and advanced and complex widgets are composed of basic widgets. The result is a large number of widgets on the page, which requires careful design of algorithms and data structures to keep the application running efficiently. Because of these additional designs, developers can easily create infinitely scrolling lists and build widgets that load on demand.