Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases now! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Flutter Design Patterns and Best Practices

You're reading from   Flutter Design Patterns and Best Practices Build scalable, maintainable, and production-ready apps using effective architectural principles

Arrow left icon
Product type Paperback
Published in Sep 2024
Publisher Packt
ISBN-13 9781801072649
Length 362 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (3):
Arrow left icon
Jaime Blasco Jaime Blasco
Author Profile Icon Jaime Blasco
Jaime Blasco
Daria Orlova Daria Orlova
Author Profile Icon Daria Orlova
Daria Orlova
Esra Kadah Esra Kadah
Author Profile Icon Esra Kadah
Esra Kadah
Arrow right icon
View More author details
Toc

Table of Contents (19) Chapters Close

Preface 1. Part 1: Building Delightful User Interfaces
2. Chapter 1: Best Practices for Building UIs with Flutter FREE CHAPTER 3. Chapter 2: Responsive UIs for All Devices 4. Part 2: Connecting UI with Business Logic
5. Chapter 3: Vanilla State Management 6. Chapter 4: State Management Patterns and Their Implementations 7. Chapter 5: Creating Consistent Navigation 8. Part 3: Exploring Practical Design Patterns and Architecture Best Practices
9. Chapter 6: The Responsible Repository Pattern 10. Chapter 7: Implementing the Inversion of Control Principle 11. Chapter 8: Ensuring Scalability and Maintainability with Layered Architecture 12. Chapter 9: Mastering Concurrent Programming in Dart 13. Chapter 10: A Bridge to the Native Side of Development 14. Part 4: Ensuring App Quality and Stability
15. Chapter 11: Unit Tests, Widget Tests, and Mocking Dependencies 16. Chapter 12: Static Code Analysis and Debugging Tools 17. Index 18. Other Books You May Enjoy

Everything is a widget! Or is it?

You have probably heard this phrase many times. It has become the slogan of Flutter – in Flutter, everything is a widget! But what is a widget and how true is this saying? At first glance, the answer might seem simple: a widget is a basic building block of UI, and everything you see on the screen is a widget. While this is true, these statements don’t provide much insight into the internals of a widget.

The framework does a good job of abstracting those details away from the developer. However, as your app grows in size and complexity, if you don’t follow best performance practices, you may start encountering issues related to frame drop. Before this can happen, let’s learn about the Flutter build system and how to make the most of it.

What is a widget?

For most of our development, we will create widgets that extend StatelessWidget or StatefulWidget. The following is the code for these:

abstract class StatelessWidget extends Widget {...}
abstract class StatefulWidget extends Widget {...}
@immutable
abstract class Widget {...}

From the source code, we can see that both of these widgets are abstract classes and that they inherit from the same class: the Widget class.

Another important place where we see the Widget class is in our build method:

Widget build(BuildContext context) {...}

This is probably the most overridden method in a Flutter application. We override it every time we create a new widget and we know that this is the method that gets called to render the UI. But how often is this method called? First of all, it can be called whenever the UI needs an update, either by the developer, for example, via setState, or by the framework, for example, on an animation ticker. Ultimately, it can be called as many times as your device can render frames in a second, which is represented by the refresh rate of your device. It usually ranges from 60 Hz to 120 Hz. This means that the build method can be called 60-120 times per second, which gives it 16-8 ms (1,000 ms / 60 frames-per-second = 16 ms or 1, 000 ms / 120 frames-per-second = 8 ms) to render the whole build method of your app. If you fail to do that, this will result in a frame drop, which might mean UI jank for the user. Usually, this doesn’t make users happy! But all developer performance optimizations aside, surely this can’t be what’s happening? Redrawing the whole application widget tree on every frame would certainly impact performance. This is not what happens in reality, so let’s find out how Flutter solves this problem.

When we look at the Widget class signature, we see that it is marked with an @immutable annotation. From a programming perspective, this means that all of the fields of this class have to be final. So after you create an instance of this class, you can’t mutate any of its fields (collections are different but let’s ignore this for now and return to it in Chapter 4). This is an interesting fact when you remember that the return type of our build method is Widget and that this method can be called up to 120 times per second. Does that mean that every time we call the build method, we will return a completely new tree of widgets? All million of them? Well, yes and no. Depending on how you build your widget tree and why and where it was updated, either the whole tree or only parts of it get rebuilt. But widgets are cheap to build. They barely have any logic and mostly serve as a data class for another Flutter tree that we will soon observe. Before we move on to this tree though, let’s take a look at one special type of widget.

Getting to know the RenderObjectWidget and its children

We have already discussed that when dealing with widgets, we mostly extend StatelessWidget and StatefulWidget. Inside the build method of our widgets, we only compose them like Lego bricks using the widgets already provided by the Flutter framework, such as Container and TextFormField, or our own widgets.

Most of the time, we only use the build method. Less often, we may use other methods such as didChangeDependencies or didUpdateWidget from the State object. Sometimes we may use our own methods, such as click handlers. This is the beauty of a declarative UI toolkit: we don’t even need to know how the UI we compose is actually rendered. We just use the API. However, in order to understand the intricacies of the Flutter build system, let’s think about it for a moment.

How many times have you used SizedBox to add some spacing between other widgets? An interesting thing about this widget is that it extends neither StatelessWidget nor StatefulWidget. It extends RenderObjectWidget. As a developer, you will rarely need to extend this widget or any other that contains RenderObjectWidget in its title. The important thing to know about this widget is that it is responsible for rendering, as the name suggests. Each child of RenderObjectWidget has an associated RenderObject field. The RenderObject class is one of the three pillars of the Flutter build system (the first being the widget and the last being the Element, which we will see in the next section). This is the class that deals with actual low-level rendering details, such as translating user intentions onto the canvas.

Let’s take a look at another example: the Text widget. Here is a piece of code for a very simple Flutter app that renders the Hello, Flutter text on the screen:

void main() {
  runApp(
    const MaterialApp(
      home: Text('Hello, Flutter'),
    ),
  );
}

We use two widgets here: the MaterialApp, which extends StatefulWidget, and Text, which extends a StatelessWidget. However, if we take a deeper look inside the Text widget, we will see that from its build method, a RichText widget is returned:

// Some code has been omitted for demo purposes
class Text extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RichText(...);
  }
}
class RichText extends MultiChildRenderObjectWidget {...}

An important difference here is that RichText extends MultiChildRenderObjectWidget, which is just a subtype of a RenderObjectWidget. So even though we didn’t do it explicitly, the last widget in our widget tree extends RenderObjectWidget. We can visualize the widget tree, and it will look something like this:

Figure 1.1 – Visual example of a widget tree

Figure 1.1 – Visual example of a widget tree

Even though you, as a developer, won’t be extending RenderObjectWidget often, you need to remember one takeaway!

This is important!

No matter how aggressively you compose your widget tree, the widgets that are actually responsible for the rendering will always extend the RenderObjectWidget class. Even if you, as a developer, don’t do it explicitly, you should know that this is what is happening deeper in the widget tree. You can always verify this by following the nesting of the build methods.

Let’s sum up what we’ve learned about the widget types:

StatelessWidget and StatefulWidget

RenderObjectWidget

Function

Composing widgets

Rendering render objects

Methods

build

createRenderObject

updateRenderObject

Extended by developer

Often

Rarely

Examples

Container, Text

SizedBox, Column

Table 1.1 – Widget differences

But if widgets are immutable, then who updates the render objects?

Unveiling the Element class

From the createRenderObject and updateRenderObject we understand that render objects are mutable. Yet the widgets themselves that create those render objects are immutable. So how can they update anything, if they are recreated every time their build method is called?

The secret lies within the Widget API itself. Let’s take a closer look at some of its methods, starting with createElement:

@immutable
abstract class Widget {
  Element createElement();
}

The first method that should interest us is createElement, which returns an Element. The element is the last of the three pillars of the Flutter build system. It does all of the shadow work, giving the spotlight to the widget. createElement gets called the first time the widget is added to the widget tree. The method calls the constructor of the overriding Element, such as StatelessElement. Let’s take a look at what happens in the constructor of the Element class:

abstract class Element {
 Widget? _widget;
 Element(Widget widget)
      :_widget = widget {...}
}

We pass the widget field as the parameter to the constructor and assign it to the local _widget field. This way, the Element retains the pointer to the underlying widget, yet the widget doesn’t retain the pointer to the element. The _widget field of the element is not final, which means that it can be reassigned. But when? The framework calls the update method of the Element any time the parent wishes to change the underlying widget. Let’s take a look inside the update method source code:

abstract class Element {
 void update(covariant Widget newWidget) {
   _widget = newWidget;
 }
}

As we can see, the pointer to the _widget field is changed to newWidget, so the old widget is thrown away, yet our element stays the same. But in order for this reassignment to happen and for this method to be called, firstly the canUpdate method of the Widget class is called. The canUpdate method checks whether the runtimeType and key of the old and new widgets are the same as follows:

abstract class Widget {
 static bool canUpdate(Widget oldWidget, Widget newWidget) {
     return oldWidget.runtimeType == newWidget.runtimeType
         && oldWidget.key == newWidget.key;
   }
}

Only if this method returns true, which means that the runtimeType and key of the old and new widgets are the same, can we update our element with a new widget. Otherwise, the whole subtree will be disregarded and a completely new element will be inserted in this place.

To better understand the flow of this process, let’s take a look at the following diagram:

Figure 1.2 – Element relationship with the widget

Figure 1.2 – Element relationship with the widget

The fact that the element can be updated instead of being recreated is even more important for the performance of RenderObjectWidget, since it deals with render objects that do the low-level painting. In the update method of RenderObjectElement, we also call updateRenderObject, which is a performance-optimized method: it only updates the render objects if there are any changes, and it only updates them partially. That’s why even though there may be many calls of the build method, it doesn’t mean that the whole tree gets completely repainted.

Finally, let’s summarize everything we’ve learned about the Flutter tree system:

Widget

Element

RenderObject

Mutable?

No

Yes

Yes

Cheap to create?

Yes

No

No

Created on every build?

Yes

No

No

Used by devs

Always

Almost never

Very rarely

Relationships

Every widget has an Element, but not every widget has a RenderObject

Implements BuildContext and has access both to the widget, and the RenderObject (if it exists)

Only created by implementers of RenderObjectWidget

Table 1.2 – Summary of widget, Element, and RenderObject roles

As we have just seen, Flutter has some straightforward yet elegant algorithms that make sure your application runs smoothly and looks flawless. Unfortunately, it doesn’t mean that the developer doesn’t have to think about performance at all, since there are many ways the performance can be impacted negatively if the best practices are ignored. Let’s take a look at how we can support Flutter in maintaining a delightful experience for our users.

You have been reading a chapter from
Flutter Design Patterns and Best Practices
Published in: Sep 2024
Publisher: Packt
ISBN-13: 9781801072649
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €18.99/month. Cancel anytime