DEV Community

loading...
Cover image for How to build a CustomScaffold with alternative build methods in Flutter

How to build a CustomScaffold with alternative build methods in Flutter

Francesco Di Donato
Frontend Software Engineer @CHILI. Constantly excited by the amount of knowledge that awaits around the corner.
Updated on ・4 min read

You learned the basics of Flutter. You already built a project of your own. But you feel it, this sensation - it is time to level up. Do not worry, we're not going too fast but a step at a time.

🎯 In this tutorial you will understand

  • how to create your own custom widget
  • how to use typdef
  • how to add alternative building methods to your custom widget

Level 1 - 🛶

The first approach we all learned was to just place a Scaffold widget and continue from there.

main.dart
class App extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    final container = Container(color: Colors.red, width: 100, height: 100);

    return MaterialApp(
      title: 'Bare Scaffold',
      home: Scaffold(body: container),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

and the result is below:

A Scaffold With a 100x100px red container partially hidden by the statusbar

We immediately notice a problem: part of the red container is hidden beneath the status bar. Here the use of SafeArea is useful:

main.dart
...
return MaterialApp(
      title: 'Scaffold with Safe Area',
      home: Scaffold(
        body: SafeArea(
          child: container,
        ),
      ),
    );
...
Enter fullscreen mode Exit fullscreen mode

that yelds the following:

A Scaffold With a 100x100px red container not hidden by the statusbar anymore

Here you catch the first drawback: following this pattern, in any further screen, you will have to repeat this code. I do not know you, but surely I do not like the repeat myself.

Level 2 - ⛵

Somewhere you heard of the DRY concept. Let's use it.

custom_scaffold.dart
import 'package:flutter/material.dart';

class CustomScaffold extends StatelessWidget {
  final Widget body;

  const CustomScaffold({this.body});

  @override
  Scaffold build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: body,
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

so that we can instantiate it like so:

main.dart
class App extends StatelessWidget {
  final container = Container(color: Colors.red, width: 100, height: 100);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Custom Scaffold .alternative',
        home: CustomScaffold(body: container));
  }
}
Enter fullscreen mode Exit fullscreen mode

Nothing changed on the output. But we spared some lines and potentially, we could spare much more of them (think at dynamically injected providers).

But now we want to find a way to easily operate on layouts and more specifically design them according to their purpose.

Level 3 - 🚤

Avoiding depriving ourselves of what we have built, let's implement an alternative way of creating the output.
LayoutBuilder is a good candidate since it gives us a wide range of constraints. But this use case is slightly different from one adopted in SafeArea. LayoutBuilder creates the output not by injecting the child; it actually uses a builder function that puts comfortable tools in our hand, constraints (other than context).
So you try to add a builder method to your own CustomScaffold, and it will also easily provide you with constraints (all without losing the fundamental functionality of SafeArea).

The first step is to create a signature for a function that will generate a Widget. Something that you normally put before params in function's declaration such as String, int, or myRandomWidget.

custom_scaffold.dart
typedef CustomBuilder = Widget Function(
  BuildContext context,
  double x,
  double y,
);
Enter fullscreen mode Exit fullscreen mode

Now get to the actual CustomScaffold. Similarly to how we defined a final Widget body (therefore also defined in the constructor), we also can define a final CustomBuilder builder. For those new to typdefs it is like adding a Function property but a more specific function, precisely defined by us.

custom_scaffold.dart
class CustomScaffold extends StatelessWidget {
  final Widget body;
  final CustomBuilder builder; #1

  const CustomScaffold({this.body, this.builder}); #2

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: LayoutBuilder(builder: (context, constrains) {
          return builder(context, constrains); #3
        }),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Adding a LayoutBuilder in the stack, we are provided with constraints that we can pass to our builder function (eventually you can consider modulating them or mixing them with something else to create something new, but for the sake of simplicity I leave this honor to you).

There is it. I don't know you, I felt sincere happiness the first time I used a HOF. Let's wrap to where we use it, so to say.

main.dart
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Scaffold .alternative',
      home: CustomScaffold(
        builder: (context, constraints) {
          return Container(
            color: Colors.blue,
            width: constraints.maxWidth / 2,
            height: constraints.minHeight / 3,
          );
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We give it a builder (to keep in mind that a Scaffold lives without problems even without a body, (🧟), it's not @required - and it gives as context and constraints. It seems fair to me.
And the result:

A Scaffold with a yellow responsive container

You see how easily you can arrange the layout, don't you?

Level 4 - 🛥

Do you know how you can use ListView() but also ListView.builder()? It's easy to implement.

custom_scaffold.dart
class CustomScaffold extends StatelessWidget {
  final Widget body;
  final CustomBuilder builder;

  const CustomScaffold({this.body, this.builder});

  static Widget responsive({CustomBuilder builder}) {
    return _buildSafeScaffold(
      child: LayoutBuilder(builder: (context, constraints) {
        return builder(context, constraints);
      }),
    );
  }

  static _buildSafeScaffold({Widget child}) {
    return Scaffold(
        body: SafeArea(
      child: child,
    ));
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: body,
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

Note how the regular build method (the one with the @override meta) is untouched - we could still instantiate CustomScaffold() and pass to it a body.

But we also get an alternative, CustomScaffold.responsive(), that gives us the constraints:

main.dart
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Scaffold .responsive',
      home: CustomScaffold.responsive(
        builder: (context, constraints) {
          return Container(
            color: Colors.yellow,
            width: constraints.maxWidth / 4,
            height: constraints.maxHeight / 2,
          );
        },
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Now that you get how to "hide" different recurring features behind your custom widget, I leave you free to experiment on your own.

We certainly won't make code history with this ability,
but if you have come this far it is very likely that this possibility was not very familiar to you. So it's a step forward. And that's enough.

Contact me

🐤 https://twitter.com/did0f

Discussion (0)