Best practices for laying out your Flutter app
Tags: flutter, mobile
Published: 2021-09-28 18:28:37

**Written by [Majid Hajian](https://blog.logrocket.com/author/majid-hajian/)** ✏️

Almost everything in Flutter is a widget, and when you compose widgets, you can build a layout. For example, you can add several widgets in a column widget to create a vertical layout. As you continue to add more widgets, the more complex your Flutter app layout will become. 

In this article, I’ll cover some best practices to implement when laying out your Flutter app.

## Using `SizedBox` instead of `Container` in Flutter

There are many use cases where you’ll need to use a placeholder. Let's look at the example below:

```dart
return _isLoaded ? Container() : YourAwesomeWidget();
```

The `Container` is a great widget that you will use extensively in Flutter. [`Container()`](https://api.flutter.dev/flutter/widgets/Container/Container.html) expands to fit the constraints provided by the parent and is not a `const` constructor. 

On the other hand, the [`SizedBox`](https://api.flutter.dev/flutter/widgets/SizedBox/SizedBox.html) is a `const` constructor and creates a fixed-size box. The [width](https://api.flutter.dev/flutter/widgets/SizedBox/width.html) and [height](https://api.flutter.dev/flutter/widgets/SizedBox/height.html) parameters can be null to indicate that the size of the box should not be constrained in the corresponding dimension. 

Hence, when we are implementing a placeholder, `SizedBox` should be used instead of `Container`.

```dart
return _isLoaded ? SizedBox() : YourAwesomeWidget();
```

## Using the `if` condition instead of ternary operator syntax

When laying out a Flutter app, it’s often the case that you want to render different widgets conditionally. You might need to generate a widget based on the platform, for example:

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

In this situation, you can drop the ternary operator and leverage Dart's built-in syntax for adding an `if` statement in an array.

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

You can also expand on this feature with a spread operator and load several widgets as needed.

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

## Considering the cost of `build()` method in Flutter

The `build` method in Flutter widgets may be invoked frequently when ancestor widgets are rebuilding the widget. It's important to avoid repetitive and costly work in `build()` methods. 

An example of this is when you use a method instead of creating widgets in your app. Let me elaborate:

```dart
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 rebuild again — which might happen frequently — all of the widgets created within the methods will also be rebuilt, leading to wasted CPU cycles and possibly memory. 

Hence, it's better to convert those methods to `StatelessWidgets` in the following way:

```dart
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 the key, widget type, and attributes, have a special cache mechanism that only rebuilds when necessary. We may even optimize these widgets by adding `const`, which leads us to the next section of this article.

## Using `const` widgets where possible

In Dart, it's good practice to use a `const` constructor where possible, and remember that the compiler will optimize your code. Now, let's review our example above. With one straightforward step, we can make the build method work even more efficiently:

```dart
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 might look simple, but it helps us avoid rebuilding the `const` widget.

## Coding `itemExtent` in `ListView` for long lists

In order to understand how to best use [`itemExtent`](https://api.flutter.dev/flutter/widgets/ListView/itemExtent.html), let's say we have a list with several thousand elements, and we need to jump to the last element when an action is triggered, e.g., when a button is clicked. Here is when `itemExtent` can drastically improve the performance of laying out of the `ListView`. 

Specifying an `itemExtent` is more efficient than letting the children determine their extent because scrolling machinery can use the foreknowledge of the children's extent to save work, like so:

```dart
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,
      ),
    );
  }
}
```

## Avoiding large trees

There is no hard rule for when to split your widget into smaller widgets. However, it's best practice to avoid large trees because of the following benefits:

*   Promotes reusability
*   Provides cleaner code
*   Enhances readability
*   Enables encapsulation
*   Offers cache mechanisms

So, you should split your code into different widgets where you can.

## Understanding constraints in Flutter

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

Let’s break this down. 

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

Then, the widget goes through its own list of children. One by one, the widget tells its children what their constraints are (which can be different for each child), and then asks each child what size it wants to be. 

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

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

Similar to the first example, a widget cannot know its own position in the screen because that’s the parent widget’s decision. 

With that said, if a child widget decides on a different size from its parent and the parent doesn’t have enough information to align it, then the child’s size might be ignored. 

Ok, let’s see this in action.

```dart
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 wish.

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

You would expect the code above to render a green `Container` with a maximum height and width of `400`. However, when you run this code, you’ll be surprised. 

![understanding parent and child constraints and containers in flutter](https://blog.logrocket.com/wp-content/uploads/2021/09/best-practices-flutter-app-container-green-screen-e1632159930796.png) 

The entire screen will be solid green! I won’t dive into the specifics here, but you might see several issues similar to this one while building your Flutter layout. 

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

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

The constraint rule will be passed from the parent widget to the child, so the child widget can decide its size within the given constraint by its parent. So, the constraints apply. 

Therefore, Flutter is passing a [tight constraint](https://api.flutter.dev/flutter/rendering/BoxConstraints/BoxConstraints.tight.html) to `MyApp()`, then `MyApp()` is passing down its tight constraint to `ConstrainedBox`. Then, `ConstrainedBox` is forced to ignore its own constraint and will be using its parent, which, in this case, is full-screen size, and that’s why you’ll see a full-screen green box. 

Typically, you’ll find that adding a `Center` widget might fix this issue. Let’s give it a try:

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

Voila! It’s fixed!   

![best practice flutter app fixed green box constraints](https://blog.logrocket.com/wp-content/uploads/2021/09/best-practices-flutter-app-e1632160060537.png) 

The `Center` widget takes a tight constraint from `MyApp()` and converts it to a [loose constraint](https://api.flutter.dev/flutter/rendering/BoxConstraints/BoxConstraints.loose.html) for its child, which is `ConstrainedBox`. Hence, `Container` follows constraints given by `ConstraiedBox` so that `Container` will have the minimum and max size applied. 

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

A tight constraint offers a single possibility — an exact size, meaning its maximum width is equal to its minimum width, and its maximum height equals its minimum height. 

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

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

A loose constraint, on the other hand, sets the maximum width and height but allows the widget to be as small as it wants. It has a minimum width and height both equal to `0`:

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

If you revisit the example above, it tells us that the `Center` allows the green `Container` to be smaller, but not larger, than the screen. The `Center` does that, of course, by passing loose constraints to the `Container`.

## Conclusion

In this article, I mentioned some of the many best practices you should put into place when you start building a Flutter application. However, there are many more — and more advanced — practices to consider, and I recommend you check out [Flutter’s thorough documentation](https://flutter.dev/docs). Happy coding.

---

## [LogRocket](https://logrocket.com/signup/): Full visibility into your web apps

[![LogRocket Dashboard Free Trial Banner](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup/)

[LogRocket](https://logrocket.com/signup/) is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

[Try it for free](https://logrocket.com/signup/).