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! 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
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
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

Understanding the Flutter layout algorithm

In Flutter, the layout phase is an essential part of the widget-building process. It is during this phase that the engine calculates how the widgets are positioned and sized on the screen. The layout phase starts after the build process that we discussed in Chapter 1 is done and is followed by the painting and composition steps of the rendering pipeline.

This layout process is critical to the overall performance and user experience of the app. If the layout is incorrect, widgets may overlap or be positioned in unexpected locations on the screen, leading to a confusing and frustrating experience for the user.

In Flutter, the layout process is handled by the RenderObject class. Each RenderObjectWidget in the widget tree has a corresponding RenderObject that is responsible for handling the layout and rendering of that widget. The RenderObject calculates its size based on information provided by its parent RenderObject and the position of its child render objects. This information passed by the parent to its child is called constraints. Let’s learn why constraints are so important.

Understanding BoxConstraints

To understand the layout process, we first need to understand the concept of constraints. In Flutter, each widget has a set of constraints that define its minimum and maximum allowable size. While the abstract version of Constraints is not bound to anything and allows the implementer to be as creative as needed, it will be easier to understand constraints if we use a more concrete example. The BoxConstraints method is the implementation that describes the constraints based on the notion of a box that can have a minimum and maximum width and height. It is used for many, if not most, of the widgets that you will encounter. There are multiple possibilities for such constraints. For example, a widget could have a minimum 0 size and a maximum infinite size. To set such constraints, we would specify them in a BoxConstraints constructor like this:

BoxConstraints(
  minWidth: 0,
  minHeight: 0,
  maxWidth: double.infinity,
  maxHeight: double.infinity,
);

These constraints would mean that the sizing options are endless and that our widget is free to choose the size it wants. On the other hand, we could be more strict and force the widget to match only a specific size, with a width of 10 and a height of 20. The BoxConstraints need to be applied to a widget that accepts constraints as a parameter, such as ConstrainedBox. We can specify a colorful child, such as a ColoredBox. Our code would look like this:

lib/example_constraints.dart

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: ConstrainedBox(
          constraints: BoxConstraints(
            minWidth: 10,
            minHeight: 20,
            maxWidth: 10,
            maxHeight: 20,
          ),
          child: ColoredBox(color: Colors.red),
        ),
      ),
    ),
  );
}

In this case, the widget will be forced to have a width of 10 and height of 20, with no other option, since the minimum allowed size is the same as the maximum. The size of the widget will always need to satisfy its constraint conditions.

Fun fact

In the preceding snippet, we have done what Container does under the hood by wrapping different kinds of box widgets based on parameters such as constraints, size, and color.

How do constraints determine the child widget’s size?

The Flutter layout algorithm can be summarized as follows:

Constraints go down. Sizes go up. Parent sets the position https://docs.flutter.dev/ui/layout/constraints

This easy and catchy rule describes the three key steps that are needed to position and lay out any widget.

Let’s break it down in more detail:

  1. Constraints go down: A widget gets its own constraints from its parent. This widget also passes new constraints to its children one by one (these can be different for each child). This process walks down the entire render tree until it reaches the last child. Once every RenderObject has constraints, the next step is to define the actual size.
  2. Sizes go up: The child picks a size that satisfies the constraints received by the parent. The sizing algorithm is defined by each RenderObject depending on their own needs. In this step, together with step 3, the framework walks back up the render tree passing the defined geometry.
  3. Parent sets the position: Once all children have had their size defined, the parent will position its children (horizontally on the x axis and vertically on the y axis) one by one. Once all children have been positioned, the parent will move to step 2 again until we reach the RenderObject root.

This can be visualized with the following diagram:

Figure 2.1 – Constraints going down from the parent to its children and sizes going up from the children to the parent

Figure 2.1 – Constraints going down from the parent to its children and sizes going up from the children to the parent

In the diagram, you can see that the constraints are passed all the way down from the root to the leaf nodes of the render object tree. Then the sizes are passed all the way up from the leaf nodes to the root. This also demonstrates the efficiency of Flutter’s layout algorithm: the layout process is a single-pass process. This means that the render tree is walked at most once in each direction: down to pass constraints and up to pass sizes. Now let’s look at some specific examples.

The parent widget limits the size of the child with the passed constraints but it is the child that decides its final size.

Let’s see this through a simple example of a container and a Text widget as its child:

 Figure 2.2 – A red container with Hello World text aligned to the top-left corner

Figure 2.2 – A red container with Hello World text aligned to the top-left corner

For brevity, we will only look at the code of the relevant widget. You can find the fully runnable example in the filename mentioned at the start of the code snippet. Until you see otherwise otherwise, keep in mind that the Container that we’re working with is a child of a Scaffold.

The code to generate this widget is as follows:

      Container(
          constraints: BoxConstraints(
            minWidth: 200,
            minHeight: 100,
            maxWidth: 200,
            maxHeight: 100,
          ),
          color: Colors.red,
          child: Text('Hello World'),
        )

In the preceding code, the Container allows any child that can fit inside it, as long as the child satisfies the constraints. As a result, the RenderObject used by Text will take these constraints into consideration when calculating its geometry.

Something interesting that you may have noticed in this code is that we set the same values for minWidth and maxWidth, as well as for minHeight and maxHeight. In terminology, this is known as tight constraints because there is quite literally no wiggle room. The size is tightly constrained. For this, BoxConstraints has a BoxConstraints.tight constructor. Instead of accepting four values, this requires just two. Code like this will produce the same result as the previous snippet:

        Container(
          constraints: BoxConstraints.tight(
            const Size(200, 100),
          ),
          color: Colors.red,
          child: Text('Hello World'),
        )

On the other hand, if we want to specify only the maximum size and let the minimum size be 0 so that the child widget can decide for itself, this is called loose constraints. In a similar way, to avoid specifying all four values, we can use the BoxConstraints.loose constructor like this:

        Container(
          constraints: BoxConstraints.loose(
            const Size(200, 100),
          ),
          color: Colors.red,
          child: Text('Hello World'),
        )

The result will be exactly the same if we’ve written it like this:

        Container(
          constraints: BoxConstraints(
            minWidth: 0,
            minHeight: 0,
            maxWidth: 200,
            maxHeight: 100,
          ),
          color: Colors.red,
          child: Text('Hello World'),
        )

With loose constraints, our text would only take up as much space as it requires. Hence, the container will do the same. Instead of being the 100x200 size, it will wrap the text tightly like this:

Figure 2.3 – A red container with Hello World text wrapped with loose constraints

Figure 2.3 – A red container with Hello World text wrapped with loose constraints

Once the child knows its size, the parent will take care of positioning it. The parent defines how its children will be allocated on the screen. It is important to remark that a child does not know its own position. In the previous example, the container decided to position the Text widget child in the top left of the screen, while the child only knows its size.

Now we can modify the position of the child by adding or modifying its parents. Imagine that we want to center the text inside the container in the previous example, as shown in the following figure.

Figure 2.4 – A red container with Hello World text aligned to the center

Figure 2.4 – A red container with Hello World text aligned to the center

As the container is the one that defines the position, we will have to either modify it or wrap the text with a more appropriate parent that has a different positioning algorithm. In this case, we will change the alignment field of this widget as follows:

lib/example_container_5.dart

   Container(
     alignment: Alignment.center,
     constraints: BoxConstraints.tight(Size(200, 100)),
     color: Colors.red,
     child: Text('Hello World'),
   );

Container is a very flexible widget and presents us with many customization options. In the preceding code, we added a property called alignment with the Alignment.center value. There are many more options such as Alignment.topRight or Alignment.bottomLeft. However, not all widgets are as flexible as Container, or maybe you can’t use the alignment property for another reason. In that case, you can wrap your child widget, as we did with our Text, in a special widget called Align. Its sole purpose is to align the child widget. Having multiple options for various use cases is great and Flutter gives us just that. Here’s how we could use the Align widget to achieve the same effect:

         Container(
          constraints: BoxConstraints.tight(
            const Size(200, 100),
          ),
          color: Colors.red,
          child: Align(
            alignment: Alignment.center,
            child: Text('Hello World'),
          ),
        ),

Aside from the Align widget, there is also a very specific one called Center, which aligns the child widget in the center and provides the developer with a more concise API.

Note

There are endless scenarios that can occur when defining constraints and positioning children. To see more examples, you can check out https://docs.flutter.dev/development/ui/layout/constraints.

Understanding the limitations of the layout rule

While the layout algorithm has quite a lot of benefits, such as only needing one pass through the whole tree, it also comes with some limitations. It is important to be aware of the limitations so that we don’t try to fight against them. Let’s see what some of them are:

  • A widget might not have its desired size: A child is forced to follow its parent’s constraints, yet the child’s desired size might not fit in those constraints, so the child’s final size might not match the desired one. One scenario where this can create conflicts is when the parent forces its child to fit a tight size. In our previous experiments with the Container widget, it has always been a child of the Scaffold. The Scaffold imposes its own constraints, which is why the results we have seen have corresponded with our expectations. However, there are use cases that might surprise you. In the following code, we set a container as the root widget, with a desired size of 10x10:

lib/example_container_7.dart

runApp(
  Container(
    height: 10,
    width: 10,
    color: Colors.red,
  ),
);

While we would expect the Container to have a 10x10 size, this won’t be the actual case and the Container will cover the full screen. It will look like this:

Figure 2.5 – A red container filling the full screen

Figure 2.5 – A red container filling the full screen

The height and width params in the preceding code are in reality a shortcut to define the constraints of its child. They are defined withBoxConstraints.tightFor(width: width, height: height).

So, let’s see what is happening in the preceding code based on what we have learned so far. First, the Container receives constraints from its parent. As this Container is the top widget, it is a special case. The RenderObject root will have a tight constraint that enforces the minimum and maximum to be the size of the screen. Therefore, when the Container is sizing itself, the only option available is the size of the screen.

  • A widget is not aware of its own position on the screen: Another interesting limitation is that we cannot learn the position of a widget from the widget itself. Its parent contains that information. While this looks like a huge disadvantage, it has a huge performance benefit for algorithms that do not have to recalculate child size each time they want to position their items (for example, GridView). In the upcoming sections of this chapter, we will learn how we can create widgets that can be positioned relative to others and therefore mitigate this inconvenience.
  • Final size and position are relative to a parent: The final consideration is that a widget’s size and position depend on its parent. This parent widget also depends on its own parent. It is impossible to define the size and position of a specific widget without taking the whole tree into consideration.

Now that we know the general rule and its limitations, let’s learn about the layout solutions that implement these basic concepts. We will learn how we can use them to create our own layouts. When in doubt, always remember: constraints go down, sizes go up, parent sets the position.

lock icon The rest of the chapter is locked
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 $19.99/month. Cancel anytime
Banner background image