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
Flutter Design Patterns and Best Practices
Flutter Design Patterns and Best Practices

Flutter Design Patterns and Best Practices: Build scalable, maintainable, and production-ready apps using effective architectural principles

Arrow left icon
Profile Icon Daria Orlova Profile Icon Jaime Blasco Profile Icon Esra Kadah
Arrow right icon
$24.99 $35.99
Full star icon Full star icon Full star icon Full star icon Full star icon 5 (3 Ratings)
eBook Sep 2024 362 pages 1st Edition
eBook
$24.99 $35.99
Paperback
$35.98 $44.99
Subscription
Free Trial
Renews at $19.99p/m
Arrow left icon
Profile Icon Daria Orlova Profile Icon Jaime Blasco Profile Icon Esra Kadah
Arrow right icon
$24.99 $35.99
Full star icon Full star icon Full star icon Full star icon Full star icon 5 (3 Ratings)
eBook Sep 2024 362 pages 1st Edition
eBook
$24.99 $35.99
Paperback
$35.98 $44.99
Subscription
Free Trial
Renews at $19.99p/m
eBook
$24.99 $35.99
Paperback
$35.98 $44.99
Subscription
Free Trial
Renews at $19.99p/m

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning
Table of content icon View table of contents Preview book icon Preview Book

Flutter Design Patterns and Best Practices

Best Practices for Building UIs with Flutter

Flutter is rapidly becoming a go-to framework for creating applications of various scales. Google Trends and Stack Overflow confirm that Flutter has become a more popular search term than React Native for the last several years (see https://trends.google.com/trends/explore?q=%2Fg%2F11f03_rzbg,%2Fg%2F11h03gfxy9&hl=en and https://insights.stackoverflow.com/trends?tags=flutter%2Creact-native). Flutter consistently appears in various development ratings: the top 3 GitHub repositories by number of contributors (https://octoverse.github.com/2022/state-of-open-source), the top 3 most downloaded plugins in JetBrains Marketplace (https://blog.jetbrains.com/platform/2024/01/jetbrains-marketplace-highlights-of-2023-major-updates-community-news/), and second in Google Play Store ratings right after Kotlin (https://appfigures.com/top-sdks/development/apps).

This isn’t a surprise, since Flutter offers a delightful toolkit that allows developers to build smooth and pixel-perfect UIs almost immediately after their first encounter with the framework. Flutter also does a great job of hiding away the details of the rendering process. However, because it is so easy to overlook those details, a lack of understanding of how the framework actually works can lead to performance issues.

This chapter explores the benefits of using Flutter’s declarative UI-building approach, as well as how that approach affects developers. We will discuss methods for optimizing performance and avoiding interference with the framework’s building and rendering processes. We will also examine how this approach works under the hood and provide best practices for creating beautiful and blazing-fast interfaces.

By the end of this chapter, you will understand the concept of the Flutter tree system and how to scope your widget tree for the best performance. This knowledge will provide the foundation necessary for learning architectural design patterns based on the framework’s build system.

In this chapter, we’re going to cover the following main topics:

  • Understanding the difference between declarative and imperative UI design.
  • Everything is a widget! Or is it?
  • Reduce, reuse, recycle!

Understanding the difference between declarative and imperative UI design

The beauty of technology is that it evolves with time based on feedback about developer experience. Today, if you’re in mobile development, there is a high chance that you have heard about Jetpack Compose, SwiftUI, React Native, and of course Flutter. The thing these technologies have in common is both that they’re used for creating mobile applications and the fact that they do it via a declarative programming approach. You may have heard this term before, but what does it actually mean and why is it important?

To take full advantage of a framework, it’s important to understand its paradigm and work with it rather than against it. Understanding the “why” behind the architectural decisions makes it much easier to understand the “how,” and to apply design patterns that complement the overall system.

Native mobile platforms have a long history of development and major transitions. In 2014, Apple announced a new language, Swift, that would replace the current Objective-C. In 2017 the Android team made Kotlin the official language for Android development, which would gradually replace Java. Those introductions had a hugely positive impact on the developer experience, yet they still had to embrace the legacy of existing framework patterns and architecture. In 2019, Google announced Jetpack Compose and Apple announced SwiftUI – completely new toolkits for building UIs. Both SwiftUI and Jetpack Compose take advantage of their respective languages, Swift and Kotlin, leaving legacy approaches behind. Both toolkits also loudly boast their declarative programming paradigm. But language advantages aside, let’s explore why declarative is now the industrial de facto and what is wrong with imperative.

Understanding the imperative paradigm

By definition, the imperative programming paradigm focuses on how to achieve the desired result. You describe the process step by step and have complete control of the process. For example, it could result in code such as this:

fun setErrorState(errorText: String) {
    val textView = findViewById<TextView>(R.id.error_text_view)
    textView.text = errorText
    textView.setTextColor(Color.RED)
    textView.visibility = View.VISIBLE
    val button = findViewById<Button>(R.id.submit_button)
    button.isEnabled = true
    val progressView = findViewById<ProgressBar>(R.id.progress_view)
    progressView.visibility = View.GONE
}

In the preceding snippet, we imperatively described how to update the UI in case of an error. We accessed the UI elements step by step and mutated their fields.

This is a real example of code that could’ve been written for a native Android application. Even though this approach may be powerful and gives the developer fine-grained control over the flow of the logic, it comes with the possibility of the following problems:

  • The more elements that can change their presentation based on a state change, the more mutations you need to handle. You can easily imagine how this simple setErrorState becomes cumbersome as more fields need to be hidden or changed. The approach also assumes that there are similar methods for handling a progress and success state. Code such as this may easily become hard to manage, especially as the amount of views in your app grows and the state becomes more complex.
  • Modifying the global state can produce side effects. On every such change, we mutate the same UI element and possibly call other methods that also mutate the same elements. The resulting myriad of nested conditionals can quickly lead to inconsistency and illegal states in the final view that the user sees. Such bugs tend to manifest only when certain conditions are met, which makes them even harder to reproduce and debug.

For many years, the imperative approach was the only way to go. Thankfully, native mobile frameworks have since started adopting declarative toolkits. Although these are great, developers who need to switch between paradigms inside of one project can encounter many challenges. Different tools require different skills and in order to be productive, the developer needs to be experienced with both. More attention needs to be paid to make sure that the application that is created with various approaches is consistent. While the new toolkits are in the process of wider adoption, some time and effort are required until they are able to fully implement what their predecessors already have. Thankfully, Flutter embraced declarative from the start.

Understanding the declarative paradigm

In an imperative approach, the focus is on the “how.” However, in the declarative approach, the focus is on the “what.” The developer describes the desired outcome, and the framework takes care of the implementation details. Since the details are abstracted by the framework, the developer has less control and has to conform to more rules. Yet the benefit of this is the elimination of the problems imposed by the imperative approach, such as excessive code and possible side effects. Let’s take a look at the following example:

Widget build(BuildContext context) {
     final isError = false;
     final isProgress = true;
     return Column(
      children: [
        MyContentView(
          showError: isError,
        ),
        Visibility(
          visible: isProgress,
          child: Center(
            child: CircularProgressIndicator(),
          ),
        ),
      ],
    );
}

In the preceding code, we have built a UI as a reaction to state changes (such as the isError or isProgress fields). In the upcoming chapters, you will learn how to elegantly handle the state, but for now, you only need to understand the concept.

This approach can also be called reactive, since the widget tree updates itself as a reaction to a change of state.

Does Flutter use the declarative or imperative paradigm?

It is important to understand that Flutter is a complex framework. Conforming to just one programming paradigm wouldn’t be practical, since it would make a lot of things harder (see https://docs.flutter.dev/resources/faq#what-programming-paradigm-does-flutters-framework-use). For example, a purely declarative approach with its natural nesting of code would, make describing a Container or Chip widget unreadable. It would also make it more complicated to manage all of their states.

Here’s an excerpt from the build method of the Container describing how to build the child widget imperatively:

  @override
  Widget build(BuildContext context) {
    Widget? current = child;
    // ...
    if (color != null) {
      current = ColoredBox(color: color!, child: current);
    }
    if (margin != null) {
      current = Padding(padding: margin!, child: current);
    }
    // ...
}

Even though the main approach of describing the widget tree can be viewed as declarative, imperative programming can be used when it feels less awkward to do so. This is why understanding the concepts, patterns, and paradigms is crucial to creating the most efficient, maintainable, and scalable solutions.

If you are coming from an imperative background, getting used to the declarative approach of building the UI may be mind-bending at first. However, shifting your focus from “how” to “what” you’re trying to build will help. Flutter can help you too, as instead of mutating each part of the UI separately, Flutter rebuilds the entire widget tree as a reaction to state changes. Yet the framework still maintains snappy performance, and developers usually don’t need to think about it much.

In the next section, let’s take a closer look at the abstraction to understand how the what actually works. We will explore not only how to use the widgets as a developer but also how the framework efficiently handles them under the hood. We will cover what to do and what not to do to avoid interfering with the building and rendering processes.

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.

Reduce, reuse, recycle!

Now that we know that the build method can be called up to 120 times per second, the question is: do we really need to call the whole build method of our app if only a small part of the widget tree has changed? The answer is no, of course not. So let’s review how we can make this happen.

First things first, let’s get one obvious but still important thing out of the way. The build method is supposed to be blazing fast. After all, it can have as little as 8 ms to run without dropping frames. This is why it’s crucial to keep any long-running tasks such as network or database requests out of this method. There are better places to do that which we will explore in detail throughout this book.

Pushing rebuilds down the tree

There can be several situations when pushing the rebuilds down the tree can impact performance in a positive way.

Calling setState of StatefulWidget

One of the most used widgets is StatefulWidget. It’s a very convenient type of widget because it can manage state changes and react to user interactions. Let’s take a look at the sample app that is created every time you start a new Flutter project: the counter app. We are interested in the code of the _MyHomePageState class, which is the State of MyHomePage:

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() { _counter++; });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            // In the original Flutter code the increment button is a
            // FloatingActionButton property of the Scaffold,
            // but for demonstration purposes, we need a slightly 
            // modified version
            TextButton(
              onPressed: _incrementCounter,
              child: const Text('Increase'),
            )
          ],
        ),
      ),
    );
  }
}

The UI is very simple. It consists of a Scaffold with an AppBar and a FloatingActionButton. Clicking the FloatingActionButton increments the internal _counter field. The body of the Scaffold is a Column with two Text widgets that describe how many times the FloatingActionButton has been clicked based on the _counter field. The preceding example differs from the original Flutter sample in one regard: instead of using the FloatingActionButton for handling clicks, we are using the TextButton. So every time we click the TextButton, the _incrementCounter method is called, which in turn calls the setState framework method and increments the _counter field. Under the hood, the setState method causes Flutter to call the build method of _MyHomePageState, which causes a rebuild. An important thing here is that setState causes a rebuild of the whole MyHomePage widget, even though we are only changing the text.

An easy way to optimize this is to push state changes down the tree by extracting them into a smaller widget. For example, we can extract everything that was inside the Center widget of Scaffold into a separate widget and call it CounterText:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Demo Home Page'),
      ),
      body: const Center(child: CounterText()),
    );
  }
}
class CounterText extends StatefulWidget {
  const CounterText({Key? key}) : super(key: key);
  @override
  State<CounterText> createState() => _CounterTextState();
}
class _CounterTextState extends State<CounterText> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() { _counter++; });
  }
  @override
  Widget build(BuildContext context) {
    return Column(...// Same code that was in the original example );
  }
}

We haven’t changed any logic. We only took the code that was inside of the Center widget of _MyHomePageState and extracted it into a separate widget:CounterText. By encapsulating the widgets that need to be rebuilt when an internal field changes into a separate widget, we ensure that whenever we call setState inside of the _CounterTextState field, only the widgets returned from the build method of _CounterTextState get rebuilt. The parent _MyHomePageState doesn’t get rebuilt, because its build method wasn’t called. We pushed the state changes down the widget tree, causing only smaller parts of the tree to get rebuilt, instead of the whole screen. In real-life app code, this scales very fast, especially if your pages are UI-heavy.

Subscribing to InheritedWidget changes via .of(context)

By extracting the changing counter text into a separate CounterText widget in the last code snippet, we have actually made one more optimization. The interesting line for us is Theme.of(context).textTheme.headlineMedium. You have certainly used Theme and other widgets, such as MediaQuery or Navigator, via the .of(context) pattern. Usually, those widgets extend a special type of class: InheritedWidget. We will look deeper into its internals in the state management part (Chapters 3 and 4), but for now, we are interested in two of its properties:

  • Instead of creating those widgets, we will access them via static getter and use some of their properties. This means that they were created somewhere higher up the tree. Hence, we will inherit them. If they weren’t and we still try to look them up, we will get an error.
  • For some of those widgets, such as Theme and MediaQuery, the .of(context) not only returns the instance of the widget if it finds one but also adds the calling widget to a set of its subscribers. When anything in this widget changes – for example, if the Theme was light and became dark – it will notify all of its subscribers and cause them to rebuild. So in the same way as with setState, if you subscribe to an InheritedWidget, changes high up in the tree will cause the rebuild of the whole widget tree starting from the widget that you have subscribed in. Push the subscription down to only those widgets that actually need it.

Extra performance tip

You may have used MediaQuery.of(context) in order to fetch information about the screen, such as its size, paddings, and view insets. Whenever you call MediaQuery.of(context), you subscribe to the whole MediaQuery widget. If you want to get updates only about the paddings (or the size, or the view insets), you can subscribe to this specific property by calling MediaQuery.paddingOf(context), MediaQuery.sizeOf(context), and so on. This is because MediaQuery actually extends a specific type of InheritedWidget – the InheritedModel widget. It allows you to subscribe only to those properties that you care about as opposed to the whole widget, which can greatly contribute to widget rebuild optimization.

Avoiding redundant rebuilds

Now that we’ve learned how to scope our trees so that only smaller sections are rebuilt, let’s find out how to minimize the amount of those rebuilds altogether.

Being mindful of the widget life cycle

Stateless widgets are boring in terms of their life cycles. Stateful widgets, on the other hand, are not. Let’s take a look at the life cycle of the State:

Figure 1.3 – Main methods of State life cycle

Figure 1.3 – Main methods of State life cycle

Here are a few things that we should care about:

  • The initState method gets called only once per widget life cycle, much like the dispose method.
  • The didChangeDependencies method gets called immediately after initState.
  • didChangeDependencies is always called when an InheritedWiget that we subscribed to has changed. This is the implementation aspect of what we have just discussed in the previous section.
  • The build method always gets called after didChangeDependencies, didUpdateWidget, and setState.

This is important!

Don’t call setState in didChangeDependencies or didUpdateWidget. Such calls are redundant, since the framework will always call build after those methods.

The best performance practices in the preceding list are also the reason why it’s better to decouple your widgets into other custom widgets rather than extract them into helper methods such as Widget buildMyWidget(). The widgets extracted into methods still access the same context or call setState, which causes the whole encapsulating widget to rebuild, so it’s generally recommended to prefer widget classes rather than methods.

One more important thing regarding the life cycle of the State is that once its dispose method has been called, it will never become alive again and we will never be able to use it again. This means that if we have acquired any resources that hold a reference to this State, such as text editing controllers, listeners, or stream subscriptions, these should be released. Otherwise, the references to these resources won’t let the garbage collector clean up this object, which will lead to memory leaks. Fortunately, it’s usually very easy to release resources by calling their own dispose or close methods inside the dispose of the State.

Caching widgets implicitly

Dart has a notion of constant constructors. We can create constant instances of classes by adding a const keyword before the class name. But when can we do this and how can we take advantage of them in Flutter?

First of all, in order to be able to declare a const constructor, all of the fields of the class must be marked as final and be known at compile time. Second, it means that if we create two objects via const constructors with the same params, such as const SizedBox(height: 16), only one instance will be created. Aside from saving some memory due to initializing fewer objects, this also provides benefits when used in a Flutter widget tree. Let’s return to our Element class once again.

We remember that the class has an update method that gets called by the framework when the underlying widget has changed its fields (but not type or key). This method changes the reference to the widget. Soon the framework calls rebuild. Since we’re working with a tree data structure, we will traverse its children. Unless your element is a leaf element, it will have children. There is a very important method in the Element API called updateChild. As the name says, it updates its children elements. But the interesting thing is how it does it:

#1 Element? updateChild(Element? child, Widget? newWidget, 
   Object? newSlot) {
#2    // A lot of code removed for demo purposes
#3
#4    final Element newChild;
#5    if (child.widget == newWidget) {
#6        newChild = child;
#7     } else if (Widget.canUpdate(child.widget, newWidget)) {
#8        child.update(newWidget);
#9        newChild = child;
#10    }
#11
#12    return newChild;
#13 }

In the preceding code, in case our current widget is the same as the new widget as determined by the == operator, we only reassign the pointer, and that’s it. By default, in Dart, the == operator returns true only if both of the instances point to the same address in memory, which is true if they were created via a const constructor with the same params.

However, if the result is false, we should check the already-familiar Widget.canUpdate. However, aside from reassigning the pointer to the new element, we also call its update method, which soon causes a rebuild.

Hence, if we use const constructors, we can avoid rebuilds of whole widget subtrees. This is also sometimes referred to as caching widgets. So use const constructors whenever possible and see whether you can extract your own widgets that can make use of const constructors, even if nested widgets can’t.

Keep in mind that you have to actually use the const constructor, not just declare it as a possibility. For example, we have a ConstText widget that has a const constructor:

class ConstText extends StatelessWidget {
  const ConstText({super.key});
  @override
  Widget build(BuildContext context) {
    return const Text('Hello World');
  }
}

However, if we create an instance of this widget without using the const constructor via the const keyword as in the following code, then we won’t get any of the benefits of the const constructor:

// Don't!
class ParentWidget extends StatelessWidget {
  const ParentWidget({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return ConstText(); // not const!
  }
}

We need to explicitly specify the const keyword when creating an instance of the class. The correct usage of the const constructor looks like this:

// Do
class ParentWidget extends StatelessWidget {
  const ParentWidget({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return const ConstText(); // const, all good
  }
}

In the preceding code, we used the const keyword during the creation of a ConstText widget. This way, we will get all of the benefits. This small keyword is very important.

Explicitly cache widgets

The same logic can be applied if the widget can’t be created with a const constructor, but can be assigned to a final field of the State. Since you’re literally saving the pointer to the same widget instance and returning it rather than creating a new one, it will follow the same execution path as the one we saw with const widgets. This is one of the ways in which you can work around the Container not being const. You might do so using the following, for example:

class _MyHomePageState extends State<MyHomePage> {
  final greenContainer = Container(
    color: Colors.green,
    height: 100,
    width: 100,
  );
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        greenContainer,
        Container(
          color: Colors.pink,
          height: 100,
          width: 100,
        ),
      ],
    );
  }
}

In the preceding code, the update method of the green container won’t be called. We have retained the reference to an already-existing widget by caching it in a local greenContainer field. Hence, we return the exact same instance as in the previous build. This falls into the case described on line 5 in the updateChild method code snippet provided earlier in this section. If the instances are the same based on the equality operator, then the update method is not called. On the other hand, the pink Container will be rebuilt every time because we create a new instance of the class every time the build method is called. This is described in line 7 of the same code snippet.

Avoiding redundant repaints

Up to this point, we have looked at tips to help you avoid causing redundant rebuilds of the widget and element trees. The truth is that the building phase is quite cheap when compared to the rendering process, as this is where all of the heavy lifting is done. Flutter optimizes this phase as much as possible, but it cannot completely control how we create our interfaces. Therefore, we may encounter cases where these optimizations are not enough or are not working effectively.

Let’s take a look at what happens when one of the render objects wants to repaint itself. We may assume that this repainting is scoped to that specific object – after all, it was the only one marked for repaint. But this is not what happens.

The thing is, even though we have scoped our widget tree, our render object tree has a relationship of its own. If a render object has been marked as needing repainting, it will not only repaint itself but also ask its parent to mark itself for repaint too. That parent then asks its parent, and so on, until the very root. And when it finally comes to painting, the object will also repaint all of its descendants. This happens until the framework encounters what is known as a repaint boundary. A repaint boundary is a Flutter way of saying “stop right here, there is nothing further to repaint.” This is done by wrapping your widget into another widget – yes, the RepaintBoundary widget.

If we wanted to depict this flow visually, it would be something like this:

Figure 1.4 – Flow of the render object’s repainting process

Figure 1.4 – Flow of the render object’s repainting process

Here is what’s happening in Figure 1.4:

  1. We start from RenderObject #12, which was the initial one to be marked for repainting.
  2. The object goes on to call parent.markNeedsPaint of RenderObject #10. Since the isRepaintBoundary field is false, the needsPaint gets set to true and goes on to ask the same for its parent.
  3. The isRepaintBoundary value of RenderObject #5 is true, so needsPaint stays false and the parent marking is stopped right there.
  4. Then the actual painting phase is started from the top widget marked as needsPaint. It traverses its children. Since isRepaintBoundary of RenderObject #11 is false, it traverses further.
  5. But isRepaintBoundary of RenderObject #15 is true, so the process is stopped right there.

So we end up repainting render objects #10, #11, and #12.

Let’s take a look at an example where a RepaintBoundary widget can be useful – in a ListView. This is the simplified version of the ListView source code:

class ListView {
  ListView({
      super.key,
      bool addRepaintBoundaries = true,
      ... // many more params
  });
  @override
  Widget? build(BuildContext context, int index) {
     if (addRepaintBoundaries) {
       child = RepaintBoundary(child: child);
     }
     return child;
  }
}

The ListView constructor accepts an addRepaintBoundaries parameter in its constructor, which by default is true. Later, when building its children, the ListView checks this flag, and if it’s true, the child widget is wrapped in a RepaintBoundary widget. This means that during scrolling, the list items don’t get repainted, which makes sense because only their offset changes, not their presentation. The RepaintBoundary widget can be extremely efficient in cases where you have a heavy yet static widget, or when only the location on the screen changes such as during scrolling, transitions, or other animations. However, like many things, it has trade-offs. In order to display the end result on the screen, the widget tree drawing instructions need to be translated into the actual pixel data. This process is called rasterization. RepaintBoundary can decide to cache the rasterized pixel values in memory, which is not limitless. Too many of them can ironically lead to performance issues.

There is also a good way to determine whether the RepaintBoundary is useful in your case. Check the diagnosis field of its renderObject via the Flutter inspector tools. If it says something along the lines of This is an outstandingly useful repaint boundary, then it’s probably a good idea to keep it.

Optimizing scroll view performance

There are two important tips for optimizing scroll view performance:

  • First, if you want to build a list of homogeneous items, the most efficient way to do so is by using the ListView.builder constructor. The beauty of this approach is that at any given time, by using the itemBuilder callback that you’ve specified, the ListView will render only those items that can actually be seen on the screen (and a tiny bit more, as determined by the cacheExtent). This means that if you have 1,000 items in your data list, you don’t need to worry about all 1,000 of them being rendered on the screen at once – unless you have set the shrinkWrap property to true.
  • This leads us to the second tip: the shrinkWrap property (available for various scroll views) forces the scroll view to calculate the layout of all its children, defeating the purpose of lazy loading. It’s often used as a quick fix for overflow errors, but there are usually better ways to address those errors without compromising performance. We’ll cover how to avoid overflow errors while maintaining performance in the next chapter.

Summary

In this chapter, we explored the relationships between the Widget, Element, and RenderObject trees. We learned how to avoid rebuilds of the Widget and Element trees by scoping the StatefulWidgets and subscriptions to inherited widgets, as well as by caching the widgets via const constructors and final initializations. We also learned how to limit repaints of the render object subtrees, as well as how to effectively work with scroll views.

In Chapter 2, we will explore how to make our already performant interfaces responsive on the ever-growing set of devices. We will cover how sizing and layout work in Flutter, how to fix overflow errors, and how to ensure that your application is usable for all users.

Left arrow icon Right arrow icon
Download code icon Download Code

Key benefits

  • Cover code structure, app architecture, testing, performance optimization, error handling, and more
  • Get hands on with code examples and exercises to apply design patterns and best practices in real-world scenarios
  • Leverage the intricacies of the Flutter framework to build better apps for your clients
  • Purchase of the print or Kindle book includes a free PDF eBook

Description

Flutter’s rapid adoption by developers and businesses alike has led to an increased demand for skilled developers who can deliver high-quality Flutter apps. Flutter can be a great tool to develop apps for yourself, but you don’t always need to consider things like scalability or business goals as a hobbyist. When you build apps for a business, however, you must use specific tools to help the business achieve its goals. This requires solutions to be fast, reliable, and delivered on time. This book will help you meet these business requirements. You'll begin by learning how to build responsive UIs that work on all devices. Next, you'll delve into state management, understanding how to select the appropriate solution and gaining hands-on experience with InheritedWidget, ChangeNotifier, and BLoC. Later, you'll move on to high-level practices such as layered architecture, dependency injection, and repository patterns. The book will also show you how to develop features of any complexity, such as multithreading and native layer implementations. You'll also learn how to ensure the quality of your solutions by prioritizing testing. By the end of this book, you'll be able to deliver well-architected Flutter projects that are stable, scalable, and maintainable.

Who is this book for?

If you’re a mobile developer who has already realized the potential of Flutter as a tool to solve business problems and are looking forward to leveling up your app-building skills, then this book is for you. Freelancers, agencies, or individuals who have already developed basic applications with Flutter and want to level up their skills to build production-ready apps at scale will find this book helpful. Basic Flutter knowledge and experience in building apps will be beneficial. Moreover, if you’re transitioning from another mobile framework, this book will make the transition easier.

What you will learn

  • Translate business requirements into technical solutions
  • Understand when and where specific design patterns would be applicable
  • Separate concerns into multiple layers with scoped responsibility
  • Build reusable architecture that can be applied to any type of app and supported by various teams
  • Debug and solve problems before they reach the user
  • Prevent production bugs by prioritizing testing during the development cycle
  • Design encapsulated features that are resilient to business requirement changes and software updates

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Sep 27, 2024
Length: 362 pages
Edition : 1st
Language : English
ISBN-13 : 9781801074551
Vendor :
Google
Category :
Languages :
Tools :

What do you get with eBook?

Product feature icon Instant access to your Digital eBook purchase
Product feature icon Download this book in EPUB and PDF formats
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
Product feature icon AI Assistant (beta) to help accelerate your learning

Product Details

Publication date : Sep 27, 2024
Length: 362 pages
Edition : 1st
Language : English
ISBN-13 : 9781801074551
Vendor :
Google
Category :
Languages :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
$199.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts
$279.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just $5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total $ 98.96 136.97 38.01 saved
Flutter Design Patterns and Best Practices
$35.98 $44.99
Java Coding Problems
$37.99 $54.99
Full-Stack Web Development with TypeScript 5
$24.99 $36.99
Total $ 98.96 136.97 38.01 saved Stars icon

Table of Contents

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

Customer reviews

Rating distribution
Full star icon Full star icon Full star icon Full star icon Full star icon 5
(3 Ratings)
5 star 100%
4 star 0%
3 star 0%
2 star 0%
1 star 0%
Patrick Ferguson Nov 20, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
This book is incredible and helps advance app development and total skills!
Feefo Verified review Feefo
Ben Oct 08, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
The level of comprehensive detail and examples in this book are impressive. For mobile app developers, this is a great way to. dive deeper into Flutter and all of it's techniques. Worth reading and practicing the topics here!
Amazon Verified review Amazon
Robert Sep 30, 2024
Full star icon Full star icon Full star icon Full star icon Full star icon 5
If you have doubts about how to relate patterns such as mvvm, mvi to Flutter. How exactly does UI creation work with the Flutter framework. The book is for you. There are a lot of useful tips but also a comprehensive look at topics such as state management, BuildContext, etc. I don't know if it will be good for a beginner. Rather for someone who has already written an application and wants to do it better in the future. Worth reading, thank you.
Subscriber review Packt
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

How do I buy and download an eBook? Chevron down icon Chevron up icon

Where there is an eBook version of a title available, you can buy it from the book details for that title. Add either the standalone eBook or the eBook and print book bundle to your shopping cart. Your eBook will show in your cart as a product on its own. After completing checkout and payment in the normal way, you will receive your receipt on the screen containing a link to a personalised PDF download file. This link will remain active for 30 days. You can download backup copies of the file by logging in to your account at any time.

If you already have Adobe reader installed, then clicking on the link will download and open the PDF file directly. If you don't, then save the PDF file on your machine and download the Reader to view it.

Please Note: Packt eBooks are non-returnable and non-refundable.

Packt eBook and Licensing When you buy an eBook from Packt Publishing, completing your purchase means you accept the terms of our licence agreement. Please read the full text of the agreement. In it we have tried to balance the need for the ebook to be usable for you the reader with our needs to protect the rights of us as Publishers and of our authors. In summary, the agreement says:

  • You may make copies of your eBook for your own use onto any machine
  • You may not pass copies of the eBook on to anyone else
How can I make a purchase on your website? Chevron down icon Chevron up icon

If you want to purchase a video course, eBook or Bundle (Print+eBook) please follow below steps:

  1. Register on our website using your email address and the password.
  2. Search for the title by name or ISBN using the search option.
  3. Select the title you want to purchase.
  4. Choose the format you wish to purchase the title in; if you order the Print Book, you get a free eBook copy of the same title. 
  5. Proceed with the checkout process (payment to be made using Credit Card, Debit Cart, or PayPal)
Where can I access support around an eBook? Chevron down icon Chevron up icon
  • If you experience a problem with using or installing Adobe Reader, the contact Adobe directly.
  • To view the errata for the book, see www.packtpub.com/support and view the pages for the title you have.
  • To view your account details or to download a new copy of the book go to www.packtpub.com/account
  • To contact us directly if a problem is not resolved, use www.packtpub.com/contact-us
What eBook formats do Packt support? Chevron down icon Chevron up icon

Our eBooks are currently available in a variety of formats such as PDF and ePubs. In the future, this may well change with trends and development in technology, but please note that our PDFs are not Adobe eBook Reader format, which has greater restrictions on security.

You will need to use Adobe Reader v9 or later in order to read Packt's PDF eBooks.

What are the benefits of eBooks? Chevron down icon Chevron up icon
  • You can get the information you need immediately
  • You can easily take them with you on a laptop
  • You can download them an unlimited number of times
  • You can print them out
  • They are copy-paste enabled
  • They are searchable
  • There is no password protection
  • They are lower price than print
  • They save resources and space
What is an eBook? Chevron down icon Chevron up icon

Packt eBooks are a complete electronic version of the print edition, available in PDF and ePub formats. Every piece of content down to the page numbering is the same. Because we save the costs of printing and shipping the book to you, we are able to offer eBooks at a lower cost than print editions.

When you have purchased an eBook, simply login to your account and click on the link in Your Download Area. We recommend you saving the file to your hard drive before opening it.

For optimal viewing of our eBooks, we recommend you download and install the free Adobe Reader version 9.