Flutter Under the Hood: Owners

Mikhail <mbixjkee> Zotyev
8 min readMay 10, 2022

--

Hello! My name is Mikhail Zotyev, I’m the Flutter Department TechLead at Surf. And this is the last part of my article series about how Flutter works under the hood:

  1. Flutter Under the Hood
  2. Flutter Under the Hood: Binding
  3. Flutter Under the Hood: Owners (you are here)

Give you a little piece of advice — for better understanding, read these articles one by one, because all of these parts tell about one of the important internal aspects of the Flutter work.

Frame building

In the first part, we talked about the interaction between Widgets, Elements, and RenderObjects. But I have described this more simply than they are: getting a new widget tree was in the moment, and all RenderObject properties changed also. But these processes are a little more complicated.

The first thing we have to know for understanding the real process is how Flutter builds the frame. The Engine asks SchedulerBinding to prepare a new frame.

SchedulerBinding’s workflow has a few stages named SchedulerPhases.

Idle. No frame is being processed in this phase. But various tasks, microtasks, etc. may be executed in this phase.

TransientCallbacks. In this phase, transient callbacks are called. Typically, these callbacks are used for animation updates.

MidFrameMicrotasks. Microtasks scheduled during the processing of transient callbacks are currently executing.

PersistentCallbacks. The persistent callbacks are currently executing. Typically, these are used for the build, layout, and paint pipelines.

PostFrameCallbacks. The post-frame callbacks are currently executing. Typically, these are used for the next frame’s work.

Four phases of these, except the idle, are used to build a new frame. It completes in ten steps:

  1. Animation step. Calling handleBeginFrame starts a procession of all callbacks which are registered in scheduleFrameCallback in the registration order. The most common of them are Tickers for AnimationControllers. And all active animations take updates this way. Why is this step very first? This is pretty simple — for most next steps we need to have actual changes by playing animations. And without it, we can’t make the layout right.
  2. Microtasks step. After handleBeginFrame ends, all scheduled microtasks start.
  3. Build step. All Elements which are marked as needing updates, rebuild in this step.
  4. Layout step. All the dirty RenderObjects have updates in this step. Also, we detect in this step, which RenderObjects need to repaint.
  5. Composition bits step. The compositing bits on any dirty RenderObjects are updated.
  6. Paint step. All RenderObjects which are marked as needing paint, are repainted in this step. After this layer tree is generated.
  7. Composition step. The layer tree is turned into a Scene (a special object that describes what we need to see) and sent to the GPU.
  8. Semantic step. All dirty render objects have updates for their semantic properties. Then a semantic tree builds. The semantic tree is used by the engine to help people within system assistive technology.
  9. Widgets layer finalization step. The widget tree is finalized by this step. All unused states and elements will be disposed of.
  10. Scheduler finalization step. In this step, all callbacks registered by addPostFrameCallback are invoked.

After all these steps we have the frame completed. But anyway, who makes all these things? The biggest part of the work is made by two manager objects.

BuildOwner

BuildOwner manages builds, updates, and all internal work with the element tree. Its responsibilities in the Build and the Widgets layer finalization steps.

In order to manage the building tree, BuildOwner needs a list of elements that are marked as needed to update. BuildOwner stores this list inside.

Let’s look at the methods which help BuildOwner to make this work.

scheduleBuildFor: make it possible to mark elements as needed to update and put this element to the dirty elements list so that it will be updated.

lockState: the mechanism which protects elements from wrong using, for example, mark as dirty while disposing of.

buildScope: establishes a scope for updating the widget tree. It uses dirty elements for this in the order of depth.

finalizeTree: finalize tree building. Destroy all elements that are no longer active. In debug mode, Flutter makes additional checking, for example checking duplicate global keys.

reassemble: This is the working horse of the HotReload mechanism. HotReload makes it possible to avoid recompiling applications again and again when we make changes and just send a new version of code to the VM.

All these methods of course use in match with then ten steps which we describe earlier. Let’s confirm this. We can see it in the WidgetsBinding.drawFrame

As we can see, the buildScope for Build step and the finalizeTree for Widgets layer finalization step. Between there we see the super.drawFrame, but about this, we will talk a little bit later, when we’ll look to another manager.

In order to put it all together, we needed to leave the BuildOwner for a few minutes. Let’s look at the Element.rebuild method. When is it called? Just three ways for this:

  1. By the BuildOwner when the tree builds.
  2. By the ComponentElement when it first time inflates to the tree. A first-time build is required for this case, because the ComponentElement just describes the part of UI, and uses other widgets for this.
  3. And the last one is when ComponentElement has an update.

The rebuild calls perfomRebuild and this one method is more interesting because it has different implementations for different elements.

For the RenderObjectElements it initiates a RenderObject update. For the ComponentElement it calls a build method. Yep, actually, that build method, which the State and the StatelessElement have.

Let’s recall what this method does. It returns the widget, which we send to the Element.updateChild. There this widget will be mounted or updated with the current element.

As far as good, we close this scheme, except one way — BuildOwner.buildScope.

In this case, we commonly call rebuild on all elements which are marked as dirty. We remember that to make an element dirty, we can use the calling scheduleBuildFor. There are not a lot of cases that lead to this:

  1. Reactivate the element from the deactivated state.
  2. At the start of the application, when attached to the render tree.
  3. By calling setState in the State.
  4. By using HotReload.
  5. And when dependencies did change (using InheritedElement).

Now we join all puzzles together — this is how BuildOwner works.

PipelineOwner

Another one manager takes a part in the preparation of the frame is the PipelineOwner. Its responsibility — working with the rendering pipeline. As we remember it works from 4 to 8 steps in our list. And WidgetsBinding.drawFrame confirms for us it again.

What is the super.drawFrame? Because the WidgetsFlutterBinding is a composition of other bindings, this method leads us to the RendererBinding.drawFrame.

As we can see in the code, calling the PipelineOwner’s methods, define the order of the frame preparation steps:

  • flushLayout for the Layout step;
  • flushCompositingBits for the Composition bits step;
  • flushPaint for the Paint step;
  • flushSemantics for the Semantic step.

Also as the BuildOwner, PipelineOwner stores inside the list of objects which needs to update. But in the PipelineOwner case, these objects are RenderObjects. They are added to this list when their property has updates. Commonly updateRenderObject called for the actualization of current properties, and if the new value is not the same as the previous, this RenderObject is added to the list.

Let’s look at the methods of the PipelineOwner, and what they do.

flushLayout: all RenderObject that mark needs to layout will be processed. The layout will be recalculated and the RenderObject will mark as needed to repaint.

flushCompositingBits: the same as previous but for special RenderObjects which take a part in composition directly.

After calling these two methods, all RenderObjects which need to be repainted will be defined.

flushPaint: this method repaints all objects which we mark as needed to repaint.

The method compositeFrame will be called on the root of the render tree at the end of the process. This will prepare the Scene object and send this scene to the engine for showing.

Layout algorithm

The important feature of layout building is how the algorithm works.

When we have a lot of objects which we need to process, we must have an effective algorithm. Flutter aims for linear performance for the initial layout and sublinear layout performance in the common case of subsequently updating an existing layout. It means that the amount of time spent on the layout should scale more slowly than the number of RenderObjects.

The layout builds once per frame, as you could notice. All objects used in this process handle twice: going down by the tree and going up. Some parts of the layout are more complex and need more iteration, but this is the exclusion rather than the rule.

In order to guarantee this performance, Flutter uses next rules:

  1. Constraints go down, from parent to the children.
  2. Sizes go up, from children to the parent.
  3. Parents position their children.

Summarize this — only info from parents to the child is a constraint, and only info from child to parent is a size. But straightforward counting is not very effective, because of it Flutter has tricky optimization:

  1. If the child object didn’t mark its own layout as needed to update, he will not recalculate while getting the same constraints.
  2. Every time when the parent calls the layout method on the child, it declares — would the size be used in its own calculation.
  3. Every time when a parent calls the layout method on a child object, the parent indicates whether it uses the size information returned by the child object. It often happens that the parent does not use this information. This means it doesn’t have to recalculate its size even if the child changes its size. It is guaranteed that the new size will comply with the existing restrictions.
  4. Tight constraints are those limits that only one allowed size can satisfy. If the maximum and minimum heights are equal, and the maximum and minimum widths are also identical, the only appropriate size is that width and height. In the case of setting tight constraints, the parent should not recalculate its size when the child recalculates its own. Even if the parent uses the child’s size in its layout because the child can’t resize without new constraints from the parent.
  5. A RenderObject may declare that it uses only the constraints provided by the parent to calculate its size. This means that the parent object of this render object does not need to be recomputed ever, except when the constraints are updated.

Conclusion

The good performance of the Flutter is not random. It is guaranteed by a complex and smart underhood mechanism.

I hope after this article series you clearly understand how Flutter works under the hood, and this knowledge helps you write efficient and high-performance code, and find elegant solutions for all problems.

Thank you for your time! Stay at the Dart side!

--

--