Learn how Flutter render widgets

Learn how Flutter render widgets

If you have ever worked with other frameworks, and then you become a Flutter guy. There might always be a question in your head that?“how flutter rendering is so fast?”. You’ll be leaving this blog post reading the last line with a statement in mind “Oh that is why flutter renders so fast”. Do you want to know how everything is done under the hood? Next line is waiting for you to read. Let’s go for it.

Watch the video or continue reading blog post.

I’m going to tell you a short story which you may definitely have heard in your flutter development journey.

“Everything in Flutter is a Widget”.

And which is undeniably true,?“Everything in Flutter is a Widget”. But as you walk with flutter, our friend?Dash?will tell you, hey! Look at this in the?documentation?of flutter is saying:

“Widget is an immutable description of a part of a user interface”.

Oh wait, are you serious? How are UI Widgets immutable ? Yes, they are immutable as in Widget implementation see the?@immutable.

No alt text provided for this image

Now, you might say well, if widgets are immutable. Then how does flutter manage the state of UI ? How does it actually represent any changes ?

Let me tell you, flutter has the concept of?trees. It does not have only one single widget tree. But there is something more than just a widget tree.

Flutter uses 3 parallel trees for rendering, which together and contribute to the performance of Flutter apps.

No alt text provided for this image

Let’s decode these trees.

Widget Tree (configure)

A widget in a widget tree represents the UI structure and configuration. In flutter we declaratively define and configure widgets, meaning that we decide how they are going to look like on the UI and they are hierarchically forming a Widget Tree, divided into parent and children.

Element Tree (manage)

An element is a corresponding Widget’s Element in the element tree created by flutter framework. An Element is Instantiation of a Widget at a particular location in the tree. A mutable piece of the tree. It manages updates, changes of the UI, and controls everything. You can think of it, it’s managing the life cycle of widgets.

Render Object Tree (paint)

A render object in Render Object Tree is responsible for the final painting of widgets on the UI. It defines how elements are visually represented on the screen. It handles layout, painting, size, and hit testing.


These three concepts, configure, manage, and paint. They map really nicely into the separation of concern that flutter has between Widgets, Elements, and RenderObjects.

Example Tree

Now, you know what each tree job is, the?Widget Tree?just describes how the UI should look based on the current state, the?Element?Tree responsible for handling updates and lifecycle, and finally The?Render Object Tree?is responsible for the final painting.

Let’s look at an example:

Padding
   padding: EdgeInsets.all(10),
   child: Text("Hello World"),
 );(        

This is a really simple example of Padding, all it does is to give padding to its child Text widget.

Let’s see how it is going to look in tree format.

No alt text provided for this image

Let’s dive deep (and see how trees are actually created)

We have again a super simple app which runs and results in a simple “Hello World” on the screen.

No alt text provided for this image

Initially the trees look something like this:

No alt text provided for this image

In flutter, when an app starts in the?main()?function, it calls the?runApp(app)?with a widget, making it the root of the widget tree. Then the next thing happens is, the widget asks the framework to create the element, then finally the element then asks the framework to create the render object for the final painting.

No alt text provided for this image

We have a?LeafRenderObjectElement?and that is the name of the element for the?Text?or a?RichText?widget, and we have a?RenderParagraph?which is the name of Render Object for the same?Text?and?RichText.

Note: Different widget might have different names of Elements and their Render Objects.

Now, the final tree structure will looks something like this:

No alt text provided for this image

Now, you might still be asking yourself, why do we have three trees while one can easily be acceptable and doing so much work for getting a small thing on the screen? So.

The magic starts to happen (when things changes)

What if the state changes in the trees? And the text inside the Text widget changes ? What would happen with the three parallel trees?

Let’s see one more example:

Here, we run the?runApp?twice just for testing and then what happens is the first runApp creates the first Text widget and the second?runApp?creates a new Text widget with different text. And after running we see the second text on the screen, but how is it actually rendered ?

No alt text provided for this image

So let’s go examine the widget tree and see what has happened when it was rendering.

When the first?runApp?was called, we got the first app Text rendered on the UI. And the 2nd?runApp?is called, a new widget pops up on to the root, and now the flutter framework will be like “what I’m gonna do in this situation? I already have created?elements?and?renderObjects”.

No alt text provided for this image

So this time the flutter framework is not going to destroy and recreate anything, but elements and renderObjects will do some work around and look what I can reuse and what I can replace?

In that case, the framework will call?canUpdate()?method first, which is comparing?oldWidget?with?newWidget?based on their?runtimeType?and keys.

Note: we’re not dealing with the keys, so it doesn’t matter in case of keys.

No alt text provided for this image

If the?runtimeType?was the same, the widget will be replaced with the same widget. Then, Element will call this?updateRenderObject()?method which is similar to above?createRenderObject()?but the only difference is that it is taking the existing values and updating the values inside it.

No alt text provided for this image

After the?updateRenderObject()?has been called on?RenderParagraph?the Text widget content will be updated and rendered on the screen. And the tree structure will become:

No alt text provided for this image

But what if the?runtimeType?was not the same? Yes, correct, it will destroy the existing Widget and recreate it from scratch. Because two different widgets with different?runtimeTypes?have different?Elements?and?RenderObjects, they cannot be replaced and reused but will be destroyed and recreated.

Did you see that?

We were not destroying and recreating the whole tree but we’re swapping immutable widgets with one another and so updating the UI, and that is the secret behind the performant apps and fast rendering in flutter?“and that is why we have parallel trees”. We do not destroy anything but instead perform reconciliation comparing oldWidget with newWidget based on their runtimeTypes and keys and… accordingly updating the widgets.

Code Example

This is a very simple app, where we have a stateful widget with a boolean switch and 3 getters (firstTree,?secondTree, and a?button). When the button is tapped it switches between the trees and in both trees for now we have the same widgets (Row,?Text,?SizedBox) but the only difference is the Text content is different.

import 'package:flutter/material.dart'
	

	void main() {
	  runApp(MyApp());
	}
	

	class MyApp extends StatelessWidget {
	  const MyApp({Key? key}) : super(key: key);
	  @override
	  Widget build(BuildContext context) {
	    return MaterialApp(
	      theme: ThemeData.dark(useMaterial3: true),
	      home: HomePage(),
	    );
	  }
	}
	

	class HomePage extends StatefulWidget {
	  const HomePage({Key? key}) : super(key: key);
	  @override
	  State<HomePage> createState() => _HomePageState();
	}
	

	class _HomePageState extends State<HomePage> {
	

	  bool _switchTree = false;
	

	  Widget get firstTree => Row(
	    mainAxisAlignment: MainAxisAlignment.center,
	    children: [
	      Text("Flutter is awesome <3", style: TextStyle(fontSize: 35),),
	      SizedBox(width: 10,),
	    ],
	  );
	

	  Widget get secondTree => Row(
	    mainAxisAlignment: MainAxisAlignment.center,
	    children: [
	      Text("Dart is awesome <3", style: TextStyle(fontSize: 35),),
	      Padding(padding: EdgeInsets.only(right: 10),),
	    ],
	  );
	

	  Widget get switchButton => Padding(
	    padding: EdgeInsets.only(bottom: 20),
	    child: ElevatedButton(
	      onPressed: () {
	        setState(() {
	          _switchTree = !_switchTree;
	        });
	      },
	      child: Text("Switch Tree"),
	    ),
	  );
	

	  @override
	  Widget build(BuildContext context) {
	    return Scaffold(
	      body: Column(
	        mainAxisAlignment: MainAxisAlignment.center,
	        children: [
	          _switchTree ? firstTree : secondTree,
	          SizedBox(height: 10,),
	          switchButton
	        ],
	      )
	    );
	  }
	};        

Run the app and Run the?Flutter DevTools?for examining the widget tree.

Go to the tab?Flutter Inspector?and click the Row in the?Widget Tree?on the Left tab, and click the?Widget Details Tree?on the Right tab.

No alt text provided for this image

On right tab, you will find every widget has a render object id represented as?renderObject#ab123?something like that.

When we first run the app?widget tree details?looks something like this:

Note: “Dart is awesome ?” text content.

No alt text provided for this image

Next, when switch button was clicked, it looks like this now:

See: “Dart is awesome ?” is changed to “Flutter is awesome ?”.

No alt text provided for this image

If you compare two image’s?renderObject?ids, you will find?NO DIFFERENCE. Everything is the same as it was before, which means nothing is destroyed, not recreated but just replaced and some updates take place.

What if??We change the second tree’s?SizedBox?to a?Padding?? like this:

Widget get secondTree => Row
 mainAxisAlignment: MainAxisAlignment.center,
 children: [
   Text("Dart is awesome <3", style: TextStyle(fontSize: 35),),
   Padding(padding: EdgeInsets.only(right: 10),),
 ],
);(        

Now, restart the app and refresh the?Widget Tree?and, you will see the?Widget Details Tree?like this:

No alt text provided for this image

The?SizedBox?widget is completely destroyed and a new?Padding?widget is created with a different?renderObject id. Because again, both widgets are different and thus, have different Element and Render Objects which cannot be replaced and reused but rather can be recreated after being destroyed.

Now what do you think? If we click back on the switch button what will happen? Absolutely right! This time the Padding widget will be destroyed and a new SizedBox will be created with a different renderObject id.

No alt text provided for this image

And make sure you take a look at other widgets such as Text and Row both have the same renderObject ids.?AND THAT IS HOW EVERYTHING HAPPENS?“UNDER THE HOOD”.

Conclusion

In conclusion, Flutter’s fast rendering capabilities can be attributed to its efficient management of the three parallel trees: Widget Tree, Element Tree, and Render Object Tree. Understanding how these trees interact and contribute to the performance and because of these trees Flutter can efficiently update and replace immutable widgets when some changes occur, without the need for full tree reconstruction.

With this understanding, you can now appreciate why Flutter’s rendering is so fast, enabling you to build performant and delightful user experiences.

Read it on Medium:

Let’s connect:

GitHub:?https://github.com/AdnanKhan45

Instagram:?https://www.instagram.com/dev.adnankhan/

YouTube:?https://youtube.com/@eTechViral

要查看或添加评论,请登录

Muhammad Adnan的更多文章

社区洞察

其他会员也浏览了