2
头图

Almost everything in Flutter is a widget, and when you write a widget, you build a layout. For example, you can add multiple widgets to a column widget to create a vertical layout. The layout of your Flutter app will become more complex as you continue to add more widgets.

In this article, I'll cover some best practices to implement when laying out Flutter applications.

Using SizedBox instead of Container in Flutter

There are many use cases where you need to use placeholders. Let's look at the following example:

return _isLoaded ? Container() : YourAwesomeWidget();

Container is a great widget that you will use extensively in Flutter. Container() expands to accommodate constraints provided by the parent and is not a const constructor.

On the other hand, SizedBox is a constructor that creates a box of fixed size. The width and height parameters can be empty to indicate that the size of the box should not be constrained by the corresponding dimensions.

Therefore, when we implement placeholders, we should use SizedBox instead of Container .

return _isLoaded ? SizedBox() : YourAwesomeWidget();

Use if conditions instead of ternary operator syntax

When laying out a Flutter application, it is often necessary to conditionally render different widgets. You may need to generate a widget depending on the platform, for example:

Row(
  children: [
    Text("Majid"),
    Platform.isAndroid ? Text("Android") : SizeBox(),
    Platform.isIOS ? Text("iOS") : SizeBox(),
  ]
);

In this case, you can remove the ternary operator and take advantage of Dart's built-in syntax to add if statement to the array.

Row(
  children: [
    Text("Majid"),
    if (Platform.isAndroid) Text("Android"),
    if (Platform.isIOS) Text("iOS"),
  ]
);

You can also extend this functionality using the spread operator and load as many widgets as you want.

Row(
  children: [
    Text("Majid"),
    if (Platform.isAndroid) Text("Android"),
    if (Platform.isIOS) ...[
      Text("iOS_1")
      Text("iOS_2")
    ],
  ]
);

Consider the cost of the build() method in Flutter

The build method in Flutter widgets may be called frequently when an ancestor widget rebuilds the widget. It is important to avoid repetitive and expensive work in the build() method.

For example, when you use methods instead of creating widgets in your application. Let me elaborate:

class MyAwesomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          _buildHeaderWidget(),
          _buildBodyWidget(context),
          _buildFooterWidget(),
        ],
      ),
    );
  }

  Widget _buildHeaderWidget() {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: FlutterLogo(
          size: 50.0,
      ),
    );
  }

  Widget _buildBodyWidget(BuildContext context) {
    return Expanded(
      child: Container(
        child: Center(
          child: Text(
            'Majid Hajian, Flutter GDE',
          ),
        ),
      ),
    );
  }

  Widget _buildFooterWidget() {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: Text('Footer'),
    );
  }
}

The downside of this approach is that when MyAwesomeWidget needs to be rebuilt again - which can happen quite often - all widgets created in the method are also rebuilt, resulting in wasted CPU cycles and possibly also memory.

So it is better to convert these methods to StatelessWidgets :

class MyAwesomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          HeaderWidget(),
          BodyWidget(),
          FooterWidget(),
        ],
      ),
    );
  }
}

class HeaderWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: FlutterLogo(
          size: 50.0,
      ),
    );
  }
}

class BodyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
        child: Center(
          child: Text(
            'Majid Hajian, Flutter GDE',
          ),
        ),
      ),
    );
  }
}

class FooterWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: Text('Footer'),
    );
  }
}

All StatefulWidgets or StatelessWidgets , based on key, widget type and attribute, have a special caching mechanism that rebuilds only when necessary. We can even optimize these widgets by adding const , which will lead us to the next part of this article.

Use const widgets whenever possible

In Dart, it's good practice to use the const constructor whenever possible, remembering that the compiler will optimize your code. Now, let's review the above example. With one straightforward step, we can make the build method work more efficiently.

class MyAwesomeWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const HeaderWidget(),
          const BodyWidget(),
          const FooterWidget(),
        ],
      ),
    );
  }
}

class HeaderWidget extends StatelessWidget {
  const HeaderWidget();
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: FlutterLogo(
          size: 50.0,
      ),
    );
  }
}

class BodyWidget extends StatelessWidget {
  const BodyWidget();
  @override
  Widget build(BuildContext context) {
    return Expanded(
      child: Container(
        child: Center(
          child: Text(
            'Majid Hajian, Flutter GDE',
          ),
        ),
      ),
    );
  }
}

class FooterWidget extends StatelessWidget {
  const FooterWidget();
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(10.0),
      child: Text('Footer'),
    );
  }
}

This change may seem simple, but it helps us avoid rebuilding the const widget.

Encoding itemExtent for long list in ListView

To understand how best to use itemExtent , suppose we have a list with a few thousand elements, and when an action is triggered, such as clicking a button, we need to jump to the last element. At this time itemExtent can greatly improve the layout performance of ListView .

Specifying itemExtent is more efficient than letting the children elements determine their ranges, because the scrolling machine can save work by using a look ahead to the children's ranges, like this:

class LongListView extends StatelessWidget {
  final _scrollController = ScrollController();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      floatingActionButton: FloatingActionButton(onPressed:() {
        _scrollController.jumpTo(
          _scrollController.position.maxScrollExtent,
        );
      }),
      body: ListView(
        controller: _scrollController,
        children: List.generate(10000, (index) => Text('Index: $index')),
        itemExtent: 400,
      ),
    );
  }
}

Avoid using large trees

There is no hard and fast rule about when to split a widget into smaller widgets. However, it is best to avoid large trees because of the following benefits:

  • promote reusability
  • Provide cleaner code
  • enhance readability
  • enable encapsulation
  • Provide caching mechanism

Therefore, you should split your code into different widgets as much as possible.

Understanding Constraints in Flutter

The golden rule of Flutter layout that every Flutter developer must know is: constraints go down, dimensions go up, parent sets position.

Let's analyze it.

A widget gets its own constraints from its parent. A constraint is just a set of four doubles: min and max width, and min and max height.

The widget then iterates over its own list of children. The widget tells its children one by one what their constraints are (which may be different for each child), and then asks each child what size it wants.

Next, the widget positions its children (on the x axis horizontally and vertically on the y axis) one by one. Finally, the widget tells its parent its own size (within the original constraints of course).

In Flutter, all widgets render themselves based on the parent or their box constraints. This brings some limitations. For example, imagine you have a child widget inside a parent widget and you want to determine its size. This widget cannot have any dimensions! Its dimensions must be within the constraints set by its parent.

Similar to the first example, the widget has no way of knowing where it is on the screen because that's the parent widget's decision.

That is, if a child widget determines a different size than its parent widget, and the parent widget does not have enough information to align it, the child widget's size may be ignored.

OK, let's look at this.

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyWidget();
  }
}

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
       constraints: const BoxConstraints(
         maxHeight: 400,
         minHeight: 100,
         minWidth: 100,
         maxWidth: 400,
       ),
      child: Container(
        color: Colors.green,
      ),
    );
  }
}

You can ignore ConstrainedBox and add the height and widget to Container if you want.

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return  Container(
      height: 400, 
      width: 400,
      color: Colors.green,
    );
  }
}

You probably want the code above to render a green Container 400 But when you run this code, you will be surprised.

The entire screen will be solid green! I won't go into the details here, but when building Flutter layouts, you may encounter several issues similar to this one.

Let's see what's going on here. In the example above, the tree looks like this:

    - `MyApp`
    - `MyWidget`
    - `ConstrainedBox`
    - `Container`

Constraint rules will be passed from the parent control to the child control, so the child control can decide its size within the given constraints of its parent control. Therefore, constraints apply.

So Flutter passes strict constraints to MyApp() , and MyApp() passes its strict constraints to ConstrainedBox . Then, ConstrainedBox forced to ignore it's own constraints and use its parent, which in this case is fullscreen size, that's why you see a fullscreen green box.

Often, you'll find that adding a Center widget might fix the problem. Let's try it out.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: MyWidget()
    );
  }
}

Voila! now it's right. It's been fixed!

Center widget takes a strict constraint from MyApp() and converts it into a loose constraint on its children, which is ConstrainedBox . Therefore, Container follows the constraints given by ConstrainedBox so that Container applies the minimum and maximum size.

Before we finish this section, let me quickly explain what tight and loose constraints are.

A strict constraint offers the possibility - an exact size, which means that its maximum width is equal to its minimum width, and its maximum height is equal to its minimum height.

If you go to Flutter's box.dart file and search for the BoxConstraints constructor, you'll find the following:

BoxConstraints.tight(Size size)
   : minWidth = size.width,
     maxWidth = size.width,
     minHeight = size.height,
     maxHeight = size.height;

On the other hand, loose constraints set a maximum width and height, but allow widgets to be as small as possible. Its minimum width and height are both equal to 0 :

BoxConstraints.loose(Size size)
   : minWidth = 0.0,
     maxWidth = size.width,
     minHeight = 0.0,
     maxHeight = size.height;

If you look at the above example again, it tells us that Center allows green Container to be smaller than the screen, but not larger. Of course, Center this by passing loose constraints to Container .

end

In this post, I've mentioned some of the many best practices you should implement when you start building your Flutter app. However, there are more -- more advanced -- practices to consider, and I recommend you check out Flutter's extensive documentation. Happy coding.


recent articles


杭州程序员张张
11.8k 声望6.7k 粉丝

Web/Flutter/独立开发者/铲屎官